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

Make internal classloader instrumentation indy compatible #12242

Merged
merged 31 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0fd0e09
Make internal classloader instrumentation indy compatible
JonasKunz Sep 13, 2024
d28efde
spotless
JonasKunz Sep 13, 2024
18b9029
Added missing comment
JonasKunz Sep 13, 2024
87f7d42
Checkstyle
JonasKunz Sep 13, 2024
116daaa
Fix test relying on injection of Constants class
JonasKunz Sep 13, 2024
106904e
Fix javadoc
JonasKunz Sep 13, 2024
f351043
Review fixes
JonasKunz Sep 13, 2024
e8a5ef2
Update instrumentation/internal/internal-class-loader/javaagent/src/m…
JonasKunz Sep 19, 2024
67b1d40
Remove redundant excludes
JonasKunz Oct 2, 2024
57d73cd
Replace copied bootstrap packages with access to BootstrapPackagesHolder
JonasKunz Oct 2, 2024
b46acad
Merge remote-tracking branch 'otel/HEAD' into indy-classloader-instru…
JonasKunz Oct 2, 2024
e54d881
Improved error output for bootstrapping recursion
JonasKunz Oct 18, 2024
393e3ef
Test: Disable eager loading of advices and see if it fails anywhere
JonasKunz Oct 18, 2024
cd7c0f9
Test 2: Remove DefineClassInstrumentation and see if anything fails
JonasKunz Oct 18, 2024
7fc8277
Revert "Test 2: Remove DefineClassInstrumentation and see if anything…
JonasKunz Oct 18, 2024
33b6130
Revert "Test: Disable eager loading of advices and see if it fails an…
JonasKunz Oct 18, 2024
2456716
Removed eager advice loading
JonasKunz Oct 22, 2024
dc26c83
Added support for nested invokedynamic bootstrapping using MutableCal…
JonasKunz Oct 22, 2024
0154cd3
Added Unit test for DefineClassInstrumentation
JonasKunz Oct 21, 2024
5f3f166
Fix problems with lambdas on recursive path
JonasKunz Oct 22, 2024
b01f387
Revert "Added Unit test for DefineClassInstrumentation"
JonasKunz Oct 22, 2024
712c70d
spotless
JonasKunz Oct 22, 2024
85beb7a
fix comments
JonasKunz Oct 22, 2024
3e03153
Remove invalid package visibility comment
JonasKunz Oct 22, 2024
05bc99f
Attempt to fix linkage errors
JonasKunz Oct 22, 2024
492c968
Merge remote-tracking branch 'otel/HEAD' into indy-classloader-instru…
JonasKunz Oct 23, 2024
6662850
Added pre-cautionary catch of linkage errors
JonasKunz Oct 23, 2024
1173824
Revert changes not needed anymore after self-review
JonasKunz Oct 23, 2024
43a4ae0
Revert unnecessary changes to AgentClassLoader
JonasKunz Oct 24, 2024
03932e4
Typo fix
JonasKunz Oct 24, 2024
add664a
Update javaagent-tooling/src/main/java/io/opentelemetry/javaagent/too…
trask Oct 24, 2024
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 @@ -47,10 +47,7 @@ public class BootDelegationInstrumentation implements TypeInstrumentation {
public ElementMatcher<TypeDescription> typeMatcher() {
// just an optimization to exclude common class loaders that are known to delegate to the
// bootstrap loader (or happen to _be_ the bootstrap loader)
return not(namedOneOf(
"java.lang.ClassLoader",
"com.ibm.oti.vm.BootstrapClassLoader",
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader"))
return not(namedOneOf("java.lang.ClassLoader", "com.ibm.oti.vm.BootstrapClassLoader"))
.and(extendsClass(named("java.lang.ClassLoader")));
}

Expand Down Expand Up @@ -136,13 +133,14 @@ public static Class<?> onEnter(@Advice.Argument(0) String name) {
return null;
}

@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void onExit(
@Advice.Return(readOnly = false) Class<?> result,
@Advice.Enter Class<?> resultFromBootstrapLoader) {
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
@Advice.Return Class<?> result, @Advice.Enter Class<?> resultFromBootstrapLoader) {
if (resultFromBootstrapLoader != null) {
result = resultFromBootstrapLoader;
return resultFromBootstrapLoader;
}
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,10 @@ public boolean defaultEnabled(ConfigProperties config) {
return true;
}

@Override
public boolean isIndyModule() {
return false;
}

@Override
public boolean isHelperClass(String className) {
// TODO: this can be removed when we drop inlined-advice support
// The advices can directly access this class in the AgentClassLoader with invokedynamic Advice
return className.equals("io.opentelemetry.javaagent.tooling.Constants");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ public static DefineClassContext onEnter(
classLoader, className, classBytes, offset, length);
}

// TODO: the ToReturned does nothing except for signaling the AdviceTransformer that it must
Copy link
Contributor

Choose a reason for hiding this comment

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

Not related to this PR but I think we need to add a method to the experimental interface that lets instrumentation module declare that it is compatible with non-inline advice so we could disable the advice rewriting for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking of it but felt like this instrumentation was a special case where this was needed and that would only be temporary until we remove support for non-indy instrumentations.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I was thinking that the main use for it would be to track the indy conversion progress and ensuring that modules that were made indy compatible once aren't broken by later changes. I'm pretty sure there was at least one more module that use the same trick to disable the advice rewriting.

// not touch this advice
// this is done because we do not want the return values to be wrapped in array types
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Enter DefineClassContext context) {
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
@Advice.Enter DefineClassContext context, @Advice.Return Class<?> returned) {
DefineClassHelper.afterDefineClass(context);
return returned;
}
}

Expand All @@ -68,9 +74,15 @@ public static DefineClassContext onEnter(
return DefineClassHelper.beforeDefineClass(classLoader, className, classBytes);
}

// TODO: the ToReturned does nothing except for signaling the AdviceTransformer that it must
// not touch this advice
// this is done because we do not want the return values to be wrapped in array types
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Enter DefineClassContext context) {
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
@Advice.Enter DefineClassContext context, @Advice.Return Class<?> returned) {
DefineClassHelper.afterDefineClass(context);
return returned;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ public static Class<?> onEnter(
}

@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void onExit(
@Advice.Return(readOnly = false) Class<?> result, @Advice.Enter Class<?> loadedClass) {
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
@Advice.Return Class<?> result, @Advice.Enter Class<?> loadedClass) {
if (loadedClass != null) {
result = loadedClass;
return loadedClass;
}
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,37 +61,37 @@ public void transform(TypeTransformer transformer) {
public static class GetResourceAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.AssignReturned.ToReturned
public static URL onExit(
@Advice.This ClassLoader classLoader,
@Advice.Argument(0) String name,
@Advice.Return(readOnly = false) URL resource) {
if (resource != null) {
return;
}

URL helper = HelperResources.loadOne(classLoader, name);
if (helper != null) {
resource = helper;
@Advice.Return URL resource) {
if (resource == null) {
URL helper = HelperResources.loadOne(classLoader, name);
if (helper != null) {
return helper;
}
}
return resource;
}
}

@SuppressWarnings("unused")
public static class GetResourcesAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.AssignReturned.ToReturned
public static Enumeration<URL> onExit(
@Advice.This ClassLoader classLoader,
@Advice.Argument(0) String name,
@Advice.Return(readOnly = false) Enumeration<URL> resources) {
@Advice.Return Enumeration<URL> resources) {
List<URL> helpers = HelperResources.loadAll(classLoader, name);
if (helpers.isEmpty()) {
return;
return resources;
}

if (!resources.hasMoreElements()) {
resources = Collections.enumeration(helpers);
return;
return Collections.enumeration(helpers);
}

List<URL> result = Collections.list(resources);
Expand All @@ -108,31 +108,30 @@ public static void onExit(
result.add(helperUrl);
}
}

resources = Collections.enumeration(result);
return Collections.enumeration(result);
}
}

@SuppressWarnings("unused")
public static class GetResourceAsStreamAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.AssignReturned.ToReturned
public static InputStream onExit(
@Advice.This ClassLoader classLoader,
@Advice.Argument(0) String name,
@Advice.Return(readOnly = false) InputStream inputStream) {
if (inputStream != null) {
return;
}

URL helper = HelperResources.loadOne(classLoader, name);
if (helper != null) {
try {
inputStream = helper.openStream();
} catch (IOException ignored) {
// ClassLoader.getResourceAsStream also ignores io exceptions from opening the stream
@Advice.Return InputStream inputStream) {
if (inputStream == null) {
URL helper = HelperResources.loadOne(classLoader, name);
if (helper != null) {
try {
return helper.openStream();
} catch (IOException ignored) {
// ClassLoader.getResourceAsStream also ignores io exceptions from opening the stream
}
}
}
return inputStream;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ public static CallSite bootstrap(
return callSite;
}

// package visibility for testing
static MethodHandle generateNoopMethodHandle(MethodType methodType) {
public static MethodHandle generateNoopMethodHandle(MethodType methodType) {
Class<?> returnType = methodType.returnType();
MethodHandle noopNoArg;
if (returnType == void.class) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.tooling.instrumentation.indy;

import java.lang.invoke.MutableCallSite;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Nullable;

class AdviceBootstrapState implements AutoCloseable {

private static final ThreadLocal<Map<Key, AdviceBootstrapState>> stateForCurrentThread =
ThreadLocal.withInitial(HashMap::new);

private final Key key;
private int recursionDepth;
@Nullable private MutableCallSite nestedCallSite;

/**
* We have to eagerly initialize to not cause a lambda construction during {@link #enter(Class,
* String, String, String, String)}.
*/
private static final Function<Key, AdviceBootstrapState> CONSTRUCTOR = AdviceBootstrapState::new;

private AdviceBootstrapState(Key key) {
this.key = key;
// enter will increment it by one, so 0 is the value for non-recursive calls
recursionDepth = -1;
}

static void initialize() {
// Eager initialize everything because we could run into recursions doing this during advice
// bootstrapping
stateForCurrentThread.get();
stateForCurrentThread.remove();
try {
Class.forName(Key.class.getName());
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
}

static AdviceBootstrapState enter(
Class<?> instrumentedClass,
String moduleClassName,
String adviceClassName,
String adviceMethodName,
String adviceMethodDescriptor) {
Key key =
new Key(
instrumentedClass,
moduleClassName,
adviceClassName,
adviceMethodName,
adviceMethodDescriptor);
AdviceBootstrapState state = stateForCurrentThread.get().computeIfAbsent(key, CONSTRUCTOR);
state.recursionDepth++;
return state;
}

public boolean isNestedInvocation() {
return recursionDepth > 0;
}

public MutableCallSite getOrInitMutableCallSite(Supplier<MutableCallSite> initializer) {
if (nestedCallSite == null) {
nestedCallSite = initializer.get();
}
return nestedCallSite;
}

public void initMutableCallSite(MutableCallSite mutableCallSite) {
if (nestedCallSite != null) {
throw new IllegalStateException("callsite has already been initialized");
}
nestedCallSite = mutableCallSite;
}

@Nullable
public MutableCallSite getMutableCallSite() {
return nestedCallSite;
}

@Override
public void close() {
if (recursionDepth == 0) {
Map<Key, AdviceBootstrapState> stateMap = stateForCurrentThread.get();
stateMap.remove(key);
if (stateMap.isEmpty()) {
// Do not leave an empty map dangling as thread local
stateForCurrentThread.remove();
}
} else {
recursionDepth--;
}
}

/** Key uniquely identifying a single invokedynamic instruction inserted for an advice */
private static class Key {

private final Class<?> instrumentedClass;
private final String moduleClassName;
private final String adviceClassName;
private final String adviceMethodName;
private final String adviceMethodDescriptor;

private Key(
Class<?> instrumentedClass,
String moduleClassName,
String adviceClassName,
String adviceMethodName,
String adviceMethodDescriptor) {
this.instrumentedClass = instrumentedClass;
this.moduleClassName = moduleClassName;
this.adviceClassName = adviceClassName;
this.adviceMethodName = adviceMethodName;
this.adviceMethodDescriptor = adviceMethodDescriptor;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || !(o instanceof Key)) {
return false;
}

Key that = (Key) o;
return instrumentedClass.equals(that.instrumentedClass)
&& moduleClassName.equals(that.moduleClassName)
&& adviceClassName.equals(that.adviceClassName)
&& adviceMethodName.equals(that.adviceMethodName)
&& adviceMethodDescriptor.equals(that.adviceMethodDescriptor);
}

@Override
public int hashCode() {
int result = instrumentedClass.hashCode();
result = 31 * result + moduleClassName.hashCode();
result = 31 * result + adviceClassName.hashCode();
result = 31 * result + adviceMethodName.hashCode();
result = 31 * result + adviceMethodDescriptor.hashCode();
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ static byte[] transform(byte[] bytes) {
}));

TransformationContext context = new TransformationContext();
if (justDelegateAdvice) {
context.disableReturnTypeChange();
}
Comment on lines +70 to +72
Copy link
Contributor

Choose a reason for hiding this comment

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

[for reviewer] without this the advice return type is assumed to be an array, which means skipOnIndex is added when using skipOn annotation attribute, and this breaks when advice is not inlined.

ClassVisitor cv =
new ClassVisitor(AsmApi.VERSION, cw) {

Expand Down
Loading
Loading