Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement invokedynamic advice bootstrapping #9382

Merged
merged 23 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
38d1825
Added basic indy bootstrapping
JonasKunz Aug 4, 2023
50b23d4
Added module name as bootstrapping arg
JonasKunz Aug 15, 2023
59d4ff6
Added integration tests
JonasKunz Aug 16, 2023
98a41ad
Rebasing / cherry picking fixes
JonasKunz Sep 4, 2023
c34d7b4
Made IndyModuleRegistry static
JonasKunz Sep 4, 2023
000c77d
Removed StackOverflow special case, further refactoring
JonasKunz Sep 4, 2023
55f6bfd
Migrated ancient bytecode instrumentation test to indy module
JonasKunz Aug 15, 2023
313c3e2
Formatting
JonasKunz Sep 4, 2023
713f25e
Replaced advice exception logging with existing mechanism
JonasKunz Sep 4, 2023
5378101
doc fixes
JonasKunz Sep 4, 2023
8dd8e24
Replaced reflection usage with MethodHandle
JonasKunz Sep 5, 2023
d3a2305
Added documentation
JonasKunz Sep 5, 2023
5fdf7cf
Removed unnecessary include
JonasKunz Sep 5, 2023
10121d3
Bootstrap dispatcher review fixes, added unit test
JonasKunz Sep 6, 2023
ce22f26
More review fixes
JonasKunz Sep 6, 2023
3ee3e56
Remove unused method
JonasKunz Sep 6, 2023
4fe49e3
spotless fix
JonasKunz Sep 6, 2023
888b6e8
Widened @AssignReturned exception suppression
JonasKunz Sep 11, 2023
214f2e4
Further review fixes
JonasKunz Sep 11, 2023
2e5ccc7
Duplicate old bytecode instrumentation test for indy modules instead …
JonasKunz Sep 11, 2023
ec327e3
Replaced CanIgnoreReturnValueSuggester check with a Otel-specific one…
JonasKunz Sep 12, 2023
c8f256b
Fix build
JonasKunz Sep 12, 2023
ab64e3a
Fix javadoc generation problems caused by AccessController usage
JonasKunz Sep 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,9 @@ tasks {
// some moving.
disable("DefaultPackage")

// we use modified OtelPrivateConstructorForUtilityClass which ignores *Advice classes
// we use modified Otel* checks which ignore *Advice classes
disable("PrivateConstructorForUtilityClass")
disable("CanIgnoreReturnValueSuggester")

// TODO(anuraaga): Remove this, probably after instrumenter API migration instead of dealing
// with older APIs.
Expand All @@ -125,9 +126,9 @@ tasks {
// Allow underscore in test-type method names
disable("MemberName")
}
if (project.path.endsWith(":testing") || name.contains("Test")) {
if ((project.path.endsWith(":testing") || name.contains("Test")) && !project.name.equals("custom-checks")) {
// This check causes too many failures, ignore the ones in tests
disable("CanIgnoreReturnValueSuggester")
disable("OtelCanIgnoreReturnValueSuggester")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.customchecks;

import static com.google.errorprone.matchers.Description.NO_MATCH;

import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.checkreturnvalue.CanIgnoreReturnValueSuggester;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.util.TreePath;

@AutoService(BugChecker.class)
@BugPattern(
summary =
"Methods with ignorable return values (including methods that always 'return this') should be annotated with @com.google.errorprone.annotations.CanIgnoreReturnValue",
severity = BugPattern.SeverityLevel.WARNING)
public class OtelCanIgnoreReturnValueSuggester extends BugChecker
implements BugChecker.MethodTreeMatcher {

private static final long serialVersionUID = 1L;

private final CanIgnoreReturnValueSuggester delegate = new CanIgnoreReturnValueSuggester();

@Override
public Description matchMethod(MethodTree methodTree, VisitorState visitorState) {
ClassTree containerClass = findContainingClass(visitorState.getPath());
if (containerClass.getSimpleName().toString().endsWith("Advice")) {
return NO_MATCH;
}
Description description = delegate.matchMethod(methodTree, visitorState);
if (description == NO_MATCH) {
return description;
}
return describeMatch(methodTree);
}

private static ClassTree findContainingClass(TreePath path) {
TreePath parent = path.getParentPath();
while (parent != null && !(parent.getLeaf() instanceof ClassTree)) {
parent = parent.getParentPath();
}
if (parent == null) {
throw new IllegalStateException(
"Method is expected to be contained in a class, something must be wrong");
}
ClassTree containerClass = (ClassTree) parent.getLeaf();
return containerClass;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ void report() {
}

// this private method is designed for assignment of the return value
@SuppressWarnings("CanIgnoreReturnValueSuggester")
@SuppressWarnings("OtelCanIgnoreReturnValueSuggester")
private SupportabilityMetrics start() {
if (agentDebugEnabled) {
ScheduledExecutorService executor =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ static final class LookupKey<K> {
private K key;
private int hashCode;

@SuppressWarnings("CanIgnoreReturnValueSuggester")
@SuppressWarnings("OtelCanIgnoreReturnValueSuggester")
LookupKey<K> withValue(K key) {
this.key = key;
hashCode = System.identityHashCode(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;

@SuppressWarnings("CanIgnoreReturnValueSuggester")
@SuppressWarnings("OtelCanIgnoreReturnValueSuggester")
enum DistributionStatisticConfigModifier {
DISABLE_HISTOGRAM_GAUGES {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap;

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;

/**
* Contains the bootstrap method for initializing invokedynamic callsites which are added via agent
* instrumentation.
*/
public class IndyBootstrapDispatcher {

private static volatile MethodHandle bootstrap;

private IndyBootstrapDispatcher() {}

/**
* Initialized the invokedynamic bootstrapping method to which this class will delegate.
*
* @param bootstrapMethod the method to delegate to. Must have the same type as {@link
* #bootstrap}.
*/
public static void init(MethodHandle bootstrapMethod) {
bootstrap = bootstrapMethod;
}

public static CallSite bootstrap(
MethodHandles.Lookup lookup,
String adviceMethodName,
MethodType adviceMethodType,
Object... args) {
CallSite callSite = null;
if (bootstrap != null) {
try {
callSite = (CallSite) bootstrap.invoke(lookup, adviceMethodName, adviceMethodType, args);
} catch (Throwable e) {
ExceptionLogger.logSuppressedError("Error bootstrapping indy instruction", e);
}
}
if (callSite == null) {
JonasKunz marked this conversation as resolved.
Show resolved Hide resolved
// The MethodHandle pointing to the Advice could not be created for some reason,
// fallback to a Noop MethodHandle to not crash the application
MethodHandle noop = generateNoopMethodHandle(adviceMethodType);
callSite = new ConstantCallSite(noop);
}
return callSite;
}

// package visibility for testing
static MethodHandle generateNoopMethodHandle(MethodType methodType) {
Class<?> returnType = methodType.returnType();
MethodHandle noopNoArg;
if (returnType == void.class) {
noopNoArg =
MethodHandles.constant(Void.class, null).asType(MethodType.methodType(void.class));
} else {
noopNoArg = MethodHandles.constant(returnType, getDefaultValue(returnType));
}
return MethodHandles.dropArguments(noopNoArg, 0, methodType.parameterList());
}

private static Object getDefaultValue(Class<?> classOrPrimitive) {
if (classOrPrimitive.isPrimitive()) {
// arrays of primitives are initialized with the correct primitive default value (e.g. 0 for
// int.class)
// we use this fact to generate the correct default value reflectively
return Array.get(Array.newInstance(classOrPrimitive, 1), 0);
} else {
return null; // null is the default value for reference types
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap;

import static org.assertj.core.api.Assertions.assertThat;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import org.junit.jupiter.api.Test;

public class IndyBootstrapDispatcherTest {

@Test
void testVoidNoopMethodHandle() throws Throwable {
MethodHandle noArg = generateAndCheck(MethodType.methodType(void.class));
noArg.invokeExact();

MethodHandle intArg = generateAndCheck(MethodType.methodType(void.class, int.class));
intArg.invokeExact(42);
}

@Test
void testIntNoopMethodHandle() throws Throwable {
MethodHandle noArg = generateAndCheck(MethodType.methodType(int.class));
assertThat((int) noArg.invokeExact()).isEqualTo(0);

MethodHandle intArg = generateAndCheck(MethodType.methodType(int.class, int.class));
assertThat((int) intArg.invokeExact(42)).isEqualTo(0);
}

@Test
void testBooleanNoopMethodHandle() throws Throwable {
MethodHandle noArg = generateAndCheck(MethodType.methodType(boolean.class));
assertThat((boolean) noArg.invokeExact()).isEqualTo(false);

MethodHandle intArg = generateAndCheck(MethodType.methodType(boolean.class, int.class));
assertThat((boolean) intArg.invokeExact(42)).isEqualTo(false);
}

@Test
void testReferenceNoopMethodHandle() throws Throwable {
MethodHandle noArg = generateAndCheck(MethodType.methodType(Runnable.class));
assertThat((Runnable) noArg.invokeExact()).isEqualTo(null);

MethodHandle intArg = generateAndCheck(MethodType.methodType(Runnable.class, int.class));
assertThat((Runnable) intArg.invokeExact(42)).isEqualTo(null);
}

private static MethodHandle generateAndCheck(MethodType type) {
MethodHandle mh = IndyBootstrapDispatcher.generateNoopMethodHandle(type);
assertThat(mh.type()).isEqualTo(type);
return mh;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import io.opentelemetry.javaagent.tooling.config.AgentConfig;
import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstaller;
import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstallerFactory;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyModuleRegistry;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyTypeTransformerImpl;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.PatchByteCodeVersionTransformer;
import io.opentelemetry.javaagent.tooling.muzzle.HelperResourceBuilderImpl;
import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationModuleMuzzle;
import io.opentelemetry.javaagent.tooling.util.IgnoreFailedTypeMatcher;
Expand Down Expand Up @@ -62,6 +65,52 @@ AgentBuilder install(
FINE, "Instrumentation {0} is disabled", instrumentationModule.instrumentationName());
return parentAgentBuilder;
}

if (instrumentationModule.isIndyModule()) {
return installIndyModule(instrumentationModule, parentAgentBuilder);
} else {
return installInjectingModule(instrumentationModule, parentAgentBuilder, config);
}
}

private AgentBuilder installIndyModule(
InstrumentationModule instrumentationModule, AgentBuilder parentAgentBuilder) {

IndyModuleRegistry.registerIndyModule(instrumentationModule);

// TODO (Jonas): Adapt MuzzleMatcher to use the same type lookup strategy as the
// InstrumentationModuleClassLoader
// MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config);
VirtualFieldImplementationInstaller contextProvider =
virtualFieldInstallerFactory.create(instrumentationModule);

AgentBuilder agentBuilder = parentAgentBuilder;
for (TypeInstrumentation typeInstrumentation : instrumentationModule.typeInstrumentations()) {
AgentBuilder.Identified.Extendable extendableAgentBuilder =
setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation)
.transform(new PatchByteCodeVersionTransformer());

IndyTypeTransformerImpl typeTransformer =
new IndyTypeTransformerImpl(extendableAgentBuilder, instrumentationModule);
typeInstrumentation.transform(typeTransformer);
extendableAgentBuilder = typeTransformer.getAgentBuilder();
// TODO (Jonas): make instrumentation of bytecode older than 1.4 opt-in via a config option
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] It could probably added later if (and only if) that proves to be an issue in practice. Having to require explicit opt-in for older bytecode means that instrumentation modules for old libraries would be disabled by default. One good example is the Apache httpclient3 library which is compiled with 1.1 bytecode.

The only issue I see with bytecode version upgrade is when the agent is unable to resolve the types that are in the stack map frames, for example when the classloader is not "cooperative enough" and prevents us from loading the class bytecode from resources.

// TODO (Jonas): we are not calling
// contextProvider.rewriteVirtualFieldsCalls(extendableAgentBuilder) anymore
// As a result the advices should store `VirtualFields` as static variables instead of having
// the lookup inline
// We need to update our documentation on that
extendableAgentBuilder = contextProvider.injectFields(extendableAgentBuilder);
JonasKunz marked this conversation as resolved.
Show resolved Hide resolved

agentBuilder = extendableAgentBuilder;
}
return agentBuilder;
}

private AgentBuilder installInjectingModule(
InstrumentationModule instrumentationModule,
AgentBuilder parentAgentBuilder,
ConfigProperties config) {
List<String> helperClassNames =
InstrumentationModuleMuzzle.getHelperClassNames(instrumentationModule);
HelperResourceBuilderImpl helperResourceBuilder = new HelperResourceBuilderImpl();
Expand All @@ -78,8 +127,6 @@ AgentBuilder install(
return parentAgentBuilder;
}

ElementMatcher.Junction<ClassLoader> moduleClassLoaderMatcher =
instrumentationModule.classLoaderMatcher();
MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config);
AgentBuilder.Transformer helperInjector =
new HelperInjector(
Expand All @@ -93,32 +140,9 @@ AgentBuilder install(

AgentBuilder agentBuilder = parentAgentBuilder;
for (TypeInstrumentation typeInstrumentation : typeInstrumentations) {
ElementMatcher<TypeDescription> typeMatcher =
new NamedMatcher<>(
instrumentationModule.getClass().getSimpleName()
+ "#"
+ typeInstrumentation.getClass().getSimpleName(),
new IgnoreFailedTypeMatcher(typeInstrumentation.typeMatcher()));
ElementMatcher<ClassLoader> classLoaderMatcher =
new NamedMatcher<>(
instrumentationModule.getClass().getSimpleName()
+ "#"
+ typeInstrumentation.getClass().getSimpleName(),
moduleClassLoaderMatcher.and(typeInstrumentation.classLoaderOptimization()));

AgentBuilder.Identified.Extendable extendableAgentBuilder =
agentBuilder
.type(
new LoggingFailSafeMatcher<>(
typeMatcher,
"Instrumentation type matcher unexpected exception: " + typeMatcher),
new LoggingFailSafeMatcher<>(
classLoaderMatcher,
"Instrumentation class loader matcher unexpected exception: "
+ classLoaderMatcher))
.and(
(typeDescription, classLoader, module, classBeingRedefined, protectionDomain) ->
classLoader == null || NOT_DECORATOR_MATCHER.matches(typeDescription))
setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation)
.and(muzzleMatcher)
.transform(ConstantAdjuster.instance())
.transform(helperInjector);
Expand All @@ -133,4 +157,37 @@ AgentBuilder install(

return agentBuilder;
}

private static AgentBuilder.Identified.Narrowable setTypeMatcher(
AgentBuilder agentBuilder,
InstrumentationModule instrumentationModule,
TypeInstrumentation typeInstrumentation) {

ElementMatcher.Junction<ClassLoader> moduleClassLoaderMatcher =
instrumentationModule.classLoaderMatcher();

ElementMatcher<TypeDescription> typeMatcher =
new NamedMatcher<>(
instrumentationModule.getClass().getSimpleName()
+ "#"
+ typeInstrumentation.getClass().getSimpleName(),
new IgnoreFailedTypeMatcher(typeInstrumentation.typeMatcher()));
ElementMatcher<ClassLoader> classLoaderMatcher =
new NamedMatcher<>(
instrumentationModule.getClass().getSimpleName()
+ "#"
+ typeInstrumentation.getClass().getSimpleName(),
moduleClassLoaderMatcher.and(typeInstrumentation.classLoaderOptimization()));

return agentBuilder
.type(
new LoggingFailSafeMatcher<>(
typeMatcher, "Instrumentation type matcher unexpected exception: " + typeMatcher),
new LoggingFailSafeMatcher<>(
classLoaderMatcher,
"Instrumentation class loader matcher unexpected exception: " + classLoaderMatcher))
.and(
(typeDescription, classLoader, module, classBeingRedefined, protectionDomain) ->
classLoader == null || NOT_DECORATOR_MATCHER.matches(typeDescription));
}
}
Loading