Add a caching API based on caffeine for use from instrumentation, not just javaagent (#2477)
* Add caching API * Finish * javadoc * Extract dep * git add * Drift * Spotbugs * checkstyle * Fix package * Test Caffeine patch
This commit is contained in:
parent
35e415c7d1
commit
d7f8967ff6
|
@ -30,6 +30,9 @@ ext {
|
|||
systemLambda : "1.1.0",
|
||||
prometheus : "0.9.0",
|
||||
assertj : '3.19.0',
|
||||
awaitility : '4.0.3',
|
||||
// Caffeine 2.x to support Java 8+. 3.x is 11+.
|
||||
caffeine : '2.9.0',
|
||||
testcontainers : '1.15.2'
|
||||
]
|
||||
|
||||
|
@ -73,6 +76,7 @@ ext {
|
|||
dependencies.create(group: 'io.prometheus', name: 'simpleclient', version: "${versions.prometheus}"),
|
||||
dependencies.create(group: 'io.prometheus', name: 'simpleclient_httpserver', version: "${versions.prometheus}"),
|
||||
],
|
||||
caffeine : "com.github.ben-manes.caffeine:caffeine:${versions.caffeine}",
|
||||
|
||||
// Testing
|
||||
|
||||
|
@ -97,5 +101,6 @@ ext {
|
|||
coroutines : dependencies.create(group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: "${versions.coroutines}"),
|
||||
junitApi : "org.junit.jupiter:junit-jupiter-api:${versions.junit5}",
|
||||
assertj : "org.assertj:assertj-core:${versions.assertj}",
|
||||
awaitility : "org.awaitility:awaitility:${versions.awaitility}"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<Or>
|
||||
<Class name="io.opentelemetry.instrumentation.test.utils.GcUtils"/>
|
||||
<Class name="io.opentelemetry.javaagent.util.GcUtils"/>
|
||||
<Class name="~io.opentelemetry.instrumentation.api.caching.CacheTest.*"/>
|
||||
</Or>
|
||||
<Bug pattern="DM_GC"/>
|
||||
</Match>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
plugins {
|
||||
id "com.github.johnrengelman.shadow"
|
||||
}
|
||||
|
||||
group = 'io.opentelemetry.instrumentation'
|
||||
|
||||
apply from: "$rootDir/gradle/java.gradle"
|
||||
apply from: "$rootDir/gradle/publish.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(deps.caffeine) {
|
||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||
exclude group: 'org.checkerframework', module: 'checker-qual'
|
||||
}
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
archiveClassifier.set("")
|
||||
|
||||
relocate "com.github.benmanes.caffeine", "io.opentelemetry.instrumentation.internal.shaded.caffeine"
|
||||
|
||||
minimize()
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
|
||||
dependsOn shadowJar
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.github.benmanes.caffeine.cache;
|
||||
|
||||
// Caffeine uses reflection to load cache implementations based on parameters specified by a user.
|
||||
// We use gradle-shadow-plugin to minimize the dependency on Caffeine, but it does not allow
|
||||
// specifying classes to keep, only artifacts. It's a relatively simple workaround for us to use
|
||||
// this non-public class to create a static link to the required implementations we use.
|
||||
final class CacheImplementations {
|
||||
|
||||
// Each type of cache has a cache implementation and a node implementation.
|
||||
|
||||
// Strong keys, strong values, maximum size
|
||||
SSMS<?, ?> ssms; // cache
|
||||
PSMS<?, ?> psms; // node
|
||||
|
||||
// Weak keys, strong values, maximum size
|
||||
WSMS<?, ?> wsms; // cache
|
||||
FSMS<?, ?> fsms; // node
|
||||
|
||||
private CacheImplementations() {}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.api.caching;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/** A cache from keys to values. */
|
||||
public interface Cache<K, V> {
|
||||
|
||||
/** Returns a new {@link CacheBuilder} to configure a {@link Cache}. */
|
||||
static CacheBuilder newBuilder() {
|
||||
return new CacheBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached value associated with the provided {@code key}. If no value is cached yet,
|
||||
* computes the value using {@code mappingFunction}, stores the result, and returns it.
|
||||
*/
|
||||
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.api.caching;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/** A builder of {@link Cache}. */
|
||||
public final class CacheBuilder {
|
||||
|
||||
private final Caffeine<?, ?> caffeine = Caffeine.newBuilder();
|
||||
|
||||
/** Sets the maximum size of the cache. */
|
||||
public CacheBuilder setMaximumSize(long maximumSize) {
|
||||
caffeine.maximumSize(maximumSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets that keys should be referenced weakly. If used, keys will use identity comparison, not
|
||||
* {@link Object#equals(Object)}.
|
||||
*/
|
||||
public CacheBuilder setWeakKeys() {
|
||||
caffeine.weakKeys();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
CacheBuilder setExecutor(Executor executor) {
|
||||
caffeine.executor(executor);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns a new {@link Cache} with the settings of this {@link CacheBuilder}. */
|
||||
public <K, V> Cache<K, V> build() {
|
||||
@SuppressWarnings("unchecked")
|
||||
com.github.benmanes.caffeine.cache.Cache<K, V> delegate =
|
||||
(com.github.benmanes.caffeine.cache.Cache<K, V>) caffeine.build();
|
||||
return new CaffeineCache<K, V>(delegate);
|
||||
}
|
||||
|
||||
CacheBuilder() {}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.api.caching;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
final class CaffeineCache<K, V> implements Cache<K, V> {
|
||||
|
||||
private final com.github.benmanes.caffeine.cache.Cache<K, V> delegate;
|
||||
|
||||
CaffeineCache(com.github.benmanes.caffeine.cache.Cache<K, V> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
|
||||
return delegate.get(key, mappingFunction);
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
Set<K> keySet() {
|
||||
return delegate.asMap().keySet();
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
void cleanup() {
|
||||
delegate.cleanUp();
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Tests for this module are in the instrumentation-api project to verify against the shaded artifact.
|
|
@ -4,6 +4,8 @@ apply from: "$rootDir/gradle/java.gradle"
|
|||
apply from: "$rootDir/gradle/publish.gradle"
|
||||
|
||||
dependencies {
|
||||
api project(":instrumentation-api-caching")
|
||||
|
||||
api deps.opentelemetryApi
|
||||
api deps.opentelemetryContext
|
||||
api deps.opentelemetrySemConv
|
||||
|
@ -16,4 +18,5 @@ dependencies {
|
|||
testImplementation project(':testing-common')
|
||||
testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.6.0'
|
||||
testImplementation deps.assertj
|
||||
testImplementation deps.awaitility
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.api.caching;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CacheTest {
|
||||
|
||||
@Nested
|
||||
class StrongKeys {
|
||||
@Test
|
||||
void unbounded() {
|
||||
Cache<String, String> cache = Cache.newBuilder().build();
|
||||
|
||||
CaffeineCache<?, ?> caffeineCache = ((CaffeineCache<?, ?>) cache);
|
||||
assertThat(cache.computeIfAbsent("cat", unused -> "meow")).isEqualTo("meow");
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
|
||||
assertThat(cache.computeIfAbsent("cat", unused -> "bark")).isEqualTo("meow");
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
|
||||
assertThat(cache.computeIfAbsent("dog", unused -> "bark")).isEqualTo("bark");
|
||||
assertThat(caffeineCache.keySet()).hasSize(2);
|
||||
assertThat(cache.computeIfAbsent("cat", unused -> "meow")).isEqualTo("meow");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bounded() {
|
||||
Cache<String, String> cache = Cache.newBuilder().setMaximumSize(1).build();
|
||||
|
||||
CaffeineCache<?, ?> caffeineCache = ((CaffeineCache<?, ?>) cache);
|
||||
assertThat(cache.computeIfAbsent("cat", unused -> "meow")).isEqualTo("meow");
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
|
||||
assertThat(cache.computeIfAbsent("cat", unused -> "bark")).isEqualTo("meow");
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
|
||||
assertThat(cache.computeIfAbsent("dog", unused -> "bark")).isEqualTo("bark");
|
||||
caffeineCache.cleanup();
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
assertThat(cache.computeIfAbsent("cat", unused -> "purr")).isEqualTo("purr");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WeakKeys {
|
||||
@Test
|
||||
void unbounded() {
|
||||
Cache<String, String> cache = Cache.newBuilder().setWeakKeys().build();
|
||||
|
||||
CaffeineCache<?, ?> caffeineCache = ((CaffeineCache<?, ?>) cache);
|
||||
String cat = new String("cat");
|
||||
String dog = new String("dog");
|
||||
assertThat(cache.computeIfAbsent(cat, unused -> "meow")).isEqualTo("meow");
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
|
||||
assertThat(cache.computeIfAbsent(cat, unused -> "bark")).isEqualTo("meow");
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
|
||||
assertThat(cache.computeIfAbsent(dog, unused -> "bark")).isEqualTo("bark");
|
||||
assertThat(caffeineCache.keySet()).hasSize(2);
|
||||
assertThat(cache.computeIfAbsent(cat, unused -> "meow")).isEqualTo("meow");
|
||||
|
||||
cat = null;
|
||||
System.gc();
|
||||
// Wait for GC to be reflected.
|
||||
await()
|
||||
.untilAsserted(
|
||||
() -> {
|
||||
caffeineCache.cleanup();
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
});
|
||||
assertThat(cache.computeIfAbsent(dog, unused -> "bark")).isEqualTo("bark");
|
||||
dog = null;
|
||||
System.gc();
|
||||
// Wait for GC to be reflected.
|
||||
await()
|
||||
.untilAsserted(
|
||||
() -> {
|
||||
caffeineCache.cleanup();
|
||||
assertThat(caffeineCache.keySet()).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void bounded() throws Exception {
|
||||
Cache<String, String> cache = Cache.newBuilder().setWeakKeys().setMaximumSize(1).build();
|
||||
|
||||
CaffeineCache<?, ?> caffeineCache = ((CaffeineCache<?, ?>) cache);
|
||||
|
||||
String cat = new String("cat");
|
||||
String dog = new String("dog");
|
||||
assertThat(cache.computeIfAbsent(cat, unused -> "meow")).isEqualTo("meow");
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
|
||||
assertThat(cache.computeIfAbsent(cat, unused -> "bark")).isEqualTo("meow");
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
|
||||
assertThat(cache.computeIfAbsent(dog, unused -> "bark")).isEqualTo("bark");
|
||||
caffeineCache.cleanup();
|
||||
assertThat(caffeineCache.keySet()).hasSize(1);
|
||||
dog = null;
|
||||
System.gc();
|
||||
// Wait for GC to be reflected.
|
||||
await()
|
||||
.untilAsserted(
|
||||
() -> {
|
||||
caffeineCache.cleanup();
|
||||
assertThat(caffeineCache.keySet()).isEmpty();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.api;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
/** An LRU cache that has a fixed maximum size. */
|
||||
public interface BoundedCache<K, V> {
|
||||
|
||||
V get(K key, Function<? super K, ? extends V> mappingFunction);
|
||||
|
||||
static <K, V> BoundedCache<K, V> build(long maxSize) {
|
||||
return Provider.get().build(maxSize);
|
||||
}
|
||||
|
||||
interface Builder {
|
||||
<K, V> BoundedCache<K, V> build(long maxSize);
|
||||
}
|
||||
|
||||
class Provider {
|
||||
/*
|
||||
The default implementation just delegates to the lookup function and should not normally be used.
|
||||
It will be replaced at startup by the AgentInstaller.
|
||||
*/
|
||||
private static final Builder NEVER_ACTUALLY_CACHES =
|
||||
new Builder() {
|
||||
@Override
|
||||
public <K, V> BoundedCache<K, V> build(long maxSize) {
|
||||
return (key, mappingFunction) -> mappingFunction.apply(key);
|
||||
}
|
||||
};
|
||||
private static final AtomicReference<Builder> builderRef =
|
||||
new AtomicReference<>(NEVER_ACTUALLY_CACHES);
|
||||
|
||||
private Provider() {}
|
||||
|
||||
public static boolean registerIfAbsent(Builder builder) {
|
||||
return builderRef.compareAndSet(NEVER_ACTUALLY_CACHES, builder);
|
||||
}
|
||||
|
||||
// Method exists for testing only
|
||||
static void reset() {
|
||||
builderRef.set(NEVER_ACTUALLY_CACHES);
|
||||
}
|
||||
|
||||
public static Builder get() {
|
||||
return builderRef.get();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ package io.opentelemetry.javaagent.instrumentation.api.db;
|
|||
|
||||
import static io.opentelemetry.javaagent.instrumentation.api.db.StatementSanitizationConfig.isStatementSanitizationEnabled;
|
||||
|
||||
import io.opentelemetry.javaagent.instrumentation.api.BoundedCache;
|
||||
import io.opentelemetry.instrumentation.api.caching.Cache;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -18,14 +18,14 @@ import org.slf4j.LoggerFactory;
|
|||
public final class SqlStatementSanitizer {
|
||||
private static final Logger log = LoggerFactory.getLogger(SqlStatementSanitizer.class);
|
||||
|
||||
private static final BoundedCache<String, SqlStatementInfo> sqlToStatementInfoCache =
|
||||
BoundedCache.build(1000);
|
||||
private static final Cache<String, SqlStatementInfo> sqlToStatementInfoCache =
|
||||
Cache.newBuilder().setMaximumSize(1000).build();
|
||||
|
||||
public static SqlStatementInfo sanitize(String statement) {
|
||||
if (!isStatementSanitizationEnabled() || statement == null) {
|
||||
return new SqlStatementInfo(statement, null, null);
|
||||
}
|
||||
return sqlToStatementInfoCache.get(
|
||||
return sqlToStatementInfoCache.computeIfAbsent(
|
||||
statement,
|
||||
k -> {
|
||||
log.trace("SQL statement cache miss");
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.api;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class BoundedCacheTest {
|
||||
private AtomicInteger cacheMisses;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
cacheMisses = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
BoundedCache.Provider.reset();
|
||||
}
|
||||
|
||||
String mockLookupFunction(String s) {
|
||||
cacheMisses.incrementAndGet();
|
||||
return s.toUpperCase();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void testCanUseBeforeRegister() {
|
||||
BoundedCache<String, String> cache = BoundedCache.build(3);
|
||||
String result1 = cache.get("foo", this::mockLookupFunction);
|
||||
String result2 = cache.get("bAr", this::mockLookupFunction);
|
||||
assertThat(result1).isEqualTo("FOO");
|
||||
assertThat(result2).isEqualTo("BAR");
|
||||
assertThat(cacheMisses.get()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void testRegisterUsesInstance() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
BoundedCache.Builder builder = buildMapBackedBuilder(map);
|
||||
BoundedCache.Provider.registerIfAbsent(builder);
|
||||
BoundedCache<String, String> cache = BoundedCache.build(3);
|
||||
String result1 = cache.get("foo", this::mockLookupFunction);
|
||||
String result2 = cache.get("fOo", this::mockLookupFunction);
|
||||
String result3 = cache.get("foo", this::mockLookupFunction);
|
||||
assertThat(result1).isEqualTo("FOO");
|
||||
assertThat(result2).isEqualTo("FOO");
|
||||
assertThat(result3).isEqualTo("FOO");
|
||||
assertThat(cacheMisses.get()).isEqualTo(2); // once for "foo" once for "fOo"
|
||||
assertThat(map.size()).isEqualTo(2);
|
||||
assertThat(map).containsKey("foo");
|
||||
assertThat(map).containsKey("fOo");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
void testRegisterMultipleFails() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
BoundedCache.Builder builder = buildMapBackedBuilder(map);
|
||||
assertThat(BoundedCache.Provider.registerIfAbsent(builder)).isTrue();
|
||||
assertThat(BoundedCache.Provider.registerIfAbsent(builder)).isFalse();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private BoundedCache.Builder buildMapBackedBuilder(Map map) {
|
||||
return new BoundedCache.Builder() {
|
||||
@Override
|
||||
public <K, V> BoundedCache<K, V> build(long maxSize) {
|
||||
return new MapBackedCache<K, V>((Map<K, V>) map);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static class MapBackedCache<K, V> implements BoundedCache<K, V> {
|
||||
private final Map<K, V> map;
|
||||
|
||||
public MapBackedCache(Map<K, V> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(K key, Function<? super K, ? extends V> mappingFunction) {
|
||||
V v = map.get(key);
|
||||
if (v == null) {
|
||||
v = mappingFunction.apply(key);
|
||||
map.put(key, v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
apply from: "$rootDir/gradle/java.gradle"
|
||||
|
||||
dependencies {
|
||||
// For testing javaagent-bootstrap's Caffeine patch, we need to compile against our cache API
|
||||
// but make sure to run against javaagent-bootstrap
|
||||
testCompileOnly project(':instrumentation-api-caching')
|
||||
testRuntimeOnly project(":javaagent-bootstrap")
|
||||
|
||||
testImplementation deps.assertj
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.api.caching;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PatchCaffeineTest {
|
||||
|
||||
@Test
|
||||
void cleanupNotForkJoinTask() {
|
||||
AtomicReference<AssertionError> errorRef = new AtomicReference<>();
|
||||
Cache<String, String> cache =
|
||||
Cache.newBuilder()
|
||||
.setExecutor(
|
||||
task -> {
|
||||
try {
|
||||
assertThat(task).isNotInstanceOf(ForkJoinTask.class);
|
||||
} catch (AssertionError e) {
|
||||
errorRef.set(e);
|
||||
}
|
||||
})
|
||||
.setMaximumSize(1)
|
||||
.build();
|
||||
assertThat(cache.computeIfAbsent("cat", unused -> "meow")).isEqualTo("meow");
|
||||
assertThat(cache.computeIfAbsent("dog", unused -> "bark")).isEqualTo("bark");
|
||||
AssertionError error = errorRef.get();
|
||||
if (error != null) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,18 @@ apply from: "$rootDir/gradle/publish.gradle"
|
|||
minimumBranchCoverage = 0.0
|
||||
minimumInstructionCoverage = 0.0
|
||||
|
||||
// patch inner class from Caffeine to avoid ForkJoinTask from being loaded too early
|
||||
sourceSets {
|
||||
patch {
|
||||
java {}
|
||||
}
|
||||
}
|
||||
jar {
|
||||
from(sourceSets.patch.output) {
|
||||
include 'io/opentelemetry/instrumentation/internal/shaded/caffeine/cache/BoundedLocalCache$PerformCleanupTask.class'
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
// classpath used by the instrumentation muzzle plugin
|
||||
instrumentationMuzzle {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Includes work from:
|
||||
/*
|
||||
* Copyright 2017 Datadog, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/*
|
||||
* Copyright 2014 Ben Manes. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.internal.shaded.caffeine.cache;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/** skeleton outer class just for compilation purposes, not included in the final patch. */
|
||||
abstract class BoundedLocalCache<K, V> {
|
||||
abstract void performCleanUp(Runnable task);
|
||||
|
||||
/** patched to not extend ForkJoinTask as we don't want that class loaded too early. */
|
||||
static final class PerformCleanupTask implements Runnable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
final WeakReference<BoundedLocalCache<?, ?>> reference;
|
||||
|
||||
PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
|
||||
reference = new WeakReference<BoundedLocalCache<?, ?>>(cache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
BoundedLocalCache<?, ?> cache = reference.get();
|
||||
if (cache != null) {
|
||||
cache.performCleanUp(/* ignored */ null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -74,8 +74,6 @@ public class AgentInstaller {
|
|||
BootstrapPackagePrefixesHolder.setBoostrapPackagePrefixes(loadBootstrapPackagePrefixes());
|
||||
// WeakMap is used by other classes below, so we need to register the provider first.
|
||||
AgentTooling.registerWeakMapProvider();
|
||||
// Instrumentation can use a bounded cache, so register here.
|
||||
AgentTooling.registerBoundedCacheProvider();
|
||||
// this needs to be done as early as possible - before the first Config.get() call
|
||||
ConfigInitializer.initialize();
|
||||
}
|
||||
|
|
|
@ -5,11 +5,8 @@
|
|||
|
||||
package io.opentelemetry.javaagent.tooling;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import io.opentelemetry.javaagent.bootstrap.WeakCache;
|
||||
import io.opentelemetry.javaagent.bootstrap.WeakCache.Provider;
|
||||
import io.opentelemetry.javaagent.instrumentation.api.BoundedCache;
|
||||
import io.opentelemetry.javaagent.instrumentation.api.WeakMap;
|
||||
import io.opentelemetry.javaagent.tooling.bytebuddy.AgentCachingPoolStrategy;
|
||||
import io.opentelemetry.javaagent.tooling.bytebuddy.AgentLocationStrategy;
|
||||
|
@ -36,21 +33,6 @@ public class AgentTooling {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of BoundCache are backed by a guava instance that lives in the agent classloader and
|
||||
* is bridged to user/instrumentation classloader through the BoundedCache.Provider interface.
|
||||
*/
|
||||
static void registerBoundedCacheProvider() {
|
||||
BoundedCache.Provider.registerIfAbsent(
|
||||
new BoundedCache.Builder() {
|
||||
@Override
|
||||
public <K, V> BoundedCache<K, V> build(long maxSize) {
|
||||
Cache<K, V> cache = CacheBuilder.newBuilder().maximumSize(maxSize).build();
|
||||
return new GuavaBoundedCache<>(cache);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static <K, V> Provider loadWeakCacheProvider() {
|
||||
Iterator<Provider> providers =
|
||||
ServiceLoader.load(Provider.class, AgentInstaller.class.getClassLoader()).iterator();
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.tooling;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import io.opentelemetry.javaagent.instrumentation.api.BoundedCache;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Function;
|
||||
|
||||
class GuavaBoundedCache<K, V> implements BoundedCache<K, V> {
|
||||
|
||||
private final Cache<K, V> delegate;
|
||||
|
||||
public GuavaBoundedCache(Cache<K, V> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(K key, Function<? super K, ? extends V> mappingFunction) {
|
||||
try {
|
||||
return delegate.get(key, () -> mappingFunction.apply(key));
|
||||
} catch (ExecutionException e) {
|
||||
throw new IllegalStateException("Unexpected cache exception", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.tooling
|
||||
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import spock.lang.Specification
|
||||
|
||||
class GuavaBoundedCacheTest extends Specification {
|
||||
|
||||
def "test cache"() {
|
||||
|
||||
when:
|
||||
|
||||
def delegate = CacheBuilder.newBuilder().maximumSize(3).build()
|
||||
def cache = new GuavaBoundedCache<>(delegate)
|
||||
def fn = { x -> x.toUpperCase() }
|
||||
|
||||
then:
|
||||
|
||||
cache.get("foo", fn) == "FOO"
|
||||
cache.get("bar", fn) == "BAR"
|
||||
cache.get("baz", fn) == "BAZ"
|
||||
cache.get("fizz", fn) == "FIZZ"
|
||||
cache.get("buzz", fn) == "BUZZ"
|
||||
delegate.size() == 3
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ rootProject.name = 'opentelemetry-java-instrumentation'
|
|||
include ':opentelemetry-api-shaded-for-instrumenting'
|
||||
include ':opentelemetry-ext-annotations-shaded-for-instrumenting'
|
||||
include ':javaagent-bootstrap'
|
||||
include ':javaagent-bootstrap-tests'
|
||||
include ':javaagent-spi'
|
||||
include ':javaagent-tooling'
|
||||
include ':javaagent'
|
||||
|
@ -42,6 +43,7 @@ include ':load-generator'
|
|||
|
||||
include ':bom-alpha'
|
||||
include ':instrumentation-api'
|
||||
include ':instrumentation-api-caching'
|
||||
include ':javaagent-api'
|
||||
|
||||
// misc
|
||||
|
|
Loading…
Reference in New Issue