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",
|
systemLambda : "1.1.0",
|
||||||
prometheus : "0.9.0",
|
prometheus : "0.9.0",
|
||||||
assertj : '3.19.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'
|
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', version: "${versions.prometheus}"),
|
||||||
dependencies.create(group: 'io.prometheus', name: 'simpleclient_httpserver', version: "${versions.prometheus}"),
|
dependencies.create(group: 'io.prometheus', name: 'simpleclient_httpserver', version: "${versions.prometheus}"),
|
||||||
],
|
],
|
||||||
|
caffeine : "com.github.ben-manes.caffeine:caffeine:${versions.caffeine}",
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
|
|
||||||
|
@ -97,5 +101,6 @@ ext {
|
||||||
coroutines : dependencies.create(group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: "${versions.coroutines}"),
|
coroutines : dependencies.create(group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: "${versions.coroutines}"),
|
||||||
junitApi : "org.junit.jupiter:junit-jupiter-api:${versions.junit5}",
|
junitApi : "org.junit.jupiter:junit-jupiter-api:${versions.junit5}",
|
||||||
assertj : "org.assertj:assertj-core:${versions.assertj}",
|
assertj : "org.assertj:assertj-core:${versions.assertj}",
|
||||||
|
awaitility : "org.awaitility:awaitility:${versions.awaitility}"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<Or>
|
<Or>
|
||||||
<Class name="io.opentelemetry.instrumentation.test.utils.GcUtils"/>
|
<Class name="io.opentelemetry.instrumentation.test.utils.GcUtils"/>
|
||||||
<Class name="io.opentelemetry.javaagent.util.GcUtils"/>
|
<Class name="io.opentelemetry.javaagent.util.GcUtils"/>
|
||||||
|
<Class name="~io.opentelemetry.instrumentation.api.caching.CacheTest.*"/>
|
||||||
</Or>
|
</Or>
|
||||||
<Bug pattern="DM_GC"/>
|
<Bug pattern="DM_GC"/>
|
||||||
</Match>
|
</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"
|
apply from: "$rootDir/gradle/publish.gradle"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api project(":instrumentation-api-caching")
|
||||||
|
|
||||||
api deps.opentelemetryApi
|
api deps.opentelemetryApi
|
||||||
api deps.opentelemetryContext
|
api deps.opentelemetryContext
|
||||||
api deps.opentelemetrySemConv
|
api deps.opentelemetrySemConv
|
||||||
|
@ -16,4 +18,5 @@ dependencies {
|
||||||
testImplementation project(':testing-common')
|
testImplementation project(':testing-common')
|
||||||
testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.6.0'
|
testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.6.0'
|
||||||
testImplementation deps.assertj
|
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 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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -18,14 +18,14 @@ import org.slf4j.LoggerFactory;
|
||||||
public final class SqlStatementSanitizer {
|
public final class SqlStatementSanitizer {
|
||||||
private static final Logger log = LoggerFactory.getLogger(SqlStatementSanitizer.class);
|
private static final Logger log = LoggerFactory.getLogger(SqlStatementSanitizer.class);
|
||||||
|
|
||||||
private static final BoundedCache<String, SqlStatementInfo> sqlToStatementInfoCache =
|
private static final Cache<String, SqlStatementInfo> sqlToStatementInfoCache =
|
||||||
BoundedCache.build(1000);
|
Cache.newBuilder().setMaximumSize(1000).build();
|
||||||
|
|
||||||
public static SqlStatementInfo sanitize(String statement) {
|
public static SqlStatementInfo sanitize(String statement) {
|
||||||
if (!isStatementSanitizationEnabled() || statement == null) {
|
if (!isStatementSanitizationEnabled() || statement == null) {
|
||||||
return new SqlStatementInfo(statement, null, null);
|
return new SqlStatementInfo(statement, null, null);
|
||||||
}
|
}
|
||||||
return sqlToStatementInfoCache.get(
|
return sqlToStatementInfoCache.computeIfAbsent(
|
||||||
statement,
|
statement,
|
||||||
k -> {
|
k -> {
|
||||||
log.trace("SQL statement cache miss");
|
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
|
minimumBranchCoverage = 0.0
|
||||||
minimumInstructionCoverage = 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 {
|
configurations {
|
||||||
// classpath used by the instrumentation muzzle plugin
|
// classpath used by the instrumentation muzzle plugin
|
||||||
instrumentationMuzzle {
|
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());
|
BootstrapPackagePrefixesHolder.setBoostrapPackagePrefixes(loadBootstrapPackagePrefixes());
|
||||||
// WeakMap is used by other classes below, so we need to register the provider first.
|
// WeakMap is used by other classes below, so we need to register the provider first.
|
||||||
AgentTooling.registerWeakMapProvider();
|
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
|
// this needs to be done as early as possible - before the first Config.get() call
|
||||||
ConfigInitializer.initialize();
|
ConfigInitializer.initialize();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,8 @@
|
||||||
|
|
||||||
package io.opentelemetry.javaagent.tooling;
|
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;
|
||||||
import io.opentelemetry.javaagent.bootstrap.WeakCache.Provider;
|
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.instrumentation.api.WeakMap;
|
||||||
import io.opentelemetry.javaagent.tooling.bytebuddy.AgentCachingPoolStrategy;
|
import io.opentelemetry.javaagent.tooling.bytebuddy.AgentCachingPoolStrategy;
|
||||||
import io.opentelemetry.javaagent.tooling.bytebuddy.AgentLocationStrategy;
|
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() {
|
private static <K, V> Provider loadWeakCacheProvider() {
|
||||||
Iterator<Provider> providers =
|
Iterator<Provider> providers =
|
||||||
ServiceLoader.load(Provider.class, AgentInstaller.class.getClassLoader()).iterator();
|
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-api-shaded-for-instrumenting'
|
||||||
include ':opentelemetry-ext-annotations-shaded-for-instrumenting'
|
include ':opentelemetry-ext-annotations-shaded-for-instrumenting'
|
||||||
include ':javaagent-bootstrap'
|
include ':javaagent-bootstrap'
|
||||||
|
include ':javaagent-bootstrap-tests'
|
||||||
include ':javaagent-spi'
|
include ':javaagent-spi'
|
||||||
include ':javaagent-tooling'
|
include ':javaagent-tooling'
|
||||||
include ':javaagent'
|
include ':javaagent'
|
||||||
|
@ -42,6 +43,7 @@ include ':load-generator'
|
||||||
|
|
||||||
include ':bom-alpha'
|
include ':bom-alpha'
|
||||||
include ':instrumentation-api'
|
include ':instrumentation-api'
|
||||||
|
include ':instrumentation-api-caching'
|
||||||
include ':javaagent-api'
|
include ':javaagent-api'
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
|
|
Loading…
Reference in New Issue