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

Conversation

JonasKunz
Copy link
Contributor

@JonasKunz JonasKunz commented Sep 13, 2024

These instrumentations were quite some fun ones.

They are slightly problematic, because they instrument ClassLoader.loadClass and ClassLoader.defineClass.
This is difficult with invokedynamic, because the bootstrapping of the invokedynamic instructions inserted for these instrumentations itself involves loading and defining of classes, causing an infinite recursion.

Here is how this PR avoid these recursions:

Avoiding ClassLoader.loadClass recursions

When an Advice is bootstrapped, ClassLoader.loadClass is invoked for loading the Advice-class and for initializing the dependencies of IndyBootsrap in case this is the very first bootstrapped advice. We ahve avoided this by:

  • excluding AgentClassLoader and InstrumentationModuleClassloader from instrumentations instrumenting ClassLoader.loadClass overriddes
  • Ensuring all AgentClassLoader and InstrumentationModuleClassloader override all overloads of ClassLoader.loadClass

The latter was needed because otherwise we would still get a recursion if invoking and overload, because it would fallback to the instrumented super method.
This is almost good enough, because it avoid the recursion for all classes loaded by the excluded loaders themselves. However, for classes bootstrap-classloader classes (such as CallDepth) used by IndyBootstrap.bootstrap we still would get a recursion, because then AgentClassLoader delegates to super.loadClass. This could occur on the very first invocation to IndyBootstrap.bootstrap if those classes are not linked yet. We avoid this by linking the corresponding classes (just CallDepth at the moment) eagerly in the static initializer of IndyBootstrap.

Avoiding ClassLoader.defineClass recursions

When an Advice is bootstrapped, the target Advice-class is loaded for the first time and therefore ClassLoader.defineClass is invoked for it. This therefore by defintion yields a invokedynamic bootstrapping recursion for the DefineClassInstrumentation.

We avoid this by adding a new mechanism to allow Advice-classes to not be loaded on the first invocation of the instrumented method, but instead already loading them when the invokedynamic bytecode is inserted. This already prevents the recursion, because then the loading of the Advice class during bootstrapping will not involve a defineClass call anymore.

Comment on lines 55 to 59
// TODO: the ToReturened does nothing except for signaling the AdviceTransformer that it must
// not touch this advice
// this is done to ensure that ClassLoaderInstrumentationModule.loadAdviceClassesEagerly works
// correctly
// we can therfore remove it, as soon as the AdviceTransformer is not applied anymore
Copy link
Contributor

Choose a reason for hiding this comment

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

[minor] maybe having a dedicated annotation for this could help, otherwise we just rely on a side-effect of having this ToReturned annotation present.

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 didn't want to introduce a separate mechanism here because this instrumentation is likely the very only case where this is needed and AdviceTransformer will be a temporary thing anyway

Comment on lines +70 to +72
if (justDelegateAdvice) {
context.disableReturnTypeChange();
}
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.

Comment on lines 96 to 100
// Ensure that CallDepth is already loaded in case of bootstrapAdvice recursions with
// ClassLoader.loadClass
// This is required because CallDepth is a bootstrap class and therefore triggers our
// ClassLoader.loadClass instrumentations
Class.forName(CallDepth.class.getName());
Copy link
Contributor

Choose a reason for hiding this comment

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

[minor] maybe add that this class needs to be explicitly initialized early because it's the one preventing recursive calls and is thus the one called first when loadClass advice method is executed. Adding a comment where this first call is made would also be relevant too just in case someone tries to add something before it.

Also, could we make this static init part of the advice static init instead ? If possible that would be easier to link with the advice method code. On the other hand this CallDepth init call might be needed elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is in fact not the case that this class needs to be initialized early. It instead needs to be linked to IndyBootstrap early:
When some IndyBootstrap.bootstrapAdvice is executed for the first time, the JVM encounters a call to CallDepth.forClass. When linking this call, the JVM itself invokes AgentClassLoader.loadClass(CallDepth) to link that callsite. This in turn will delegate to the instrumented ClassLoader.loadClass because it is a bootstrap class. It doesn't matter where within bootstrapAdvice this call happens, it just happened by chance that it is the first one.

Also, could we make this static init part of the advice static init instead

For the reasons above: No, this is not possible, because it wouldn't link the usage of CallDepth from within the IndyBootstrap class.

@JonasKunz JonasKunz marked this pull request as ready for review September 13, 2024 14:23
@JonasKunz JonasKunz requested a review from a team September 13, 2024 14:23
@JonasKunz JonasKunz force-pushed the indy-classloader-instrumentations branch from ac1ae8a to f351043 Compare September 16, 2024 07:09
@JonasKunz JonasKunz requested a review from a team September 19, 2024 08:44
…ain/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/BootDelegationInstrumentation.java

Co-authored-by: SylvainJuge <763082+SylvainJuge@users.noreply.github.com>
@JonasKunz JonasKunz requested a review from a team as a code owner October 2, 2024 10:08
@@ -52,9 +52,17 @@ 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.

Comment on lines 170 to 177
try {
clazz = Class.forName(name, false, this.getParent());
} catch (ClassNotFoundException e) {
// ignore
}
}
if (clazz == null) {
clazz = super.findClass(name);
Copy link
Contributor

Choose a reason for hiding this comment

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

After changing the InstrumentationModuleClassLoader.defineClassWithPackage this probably isn't necessary any more. Either revert or add a comment that this is here to avoid calling super.loadClass.

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've also removed the loadClass override in AgentClassLoader. Remove it in InstrumentationModuleClassLoader is not possible, because then the LoadClassAdvice itself becomes recursive (not the bootstrapping this time):

[otel.javaagent 2024-10-24 12:42:55:462 +0200] [main] DEBUG io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyBootstrap - Fixing nested instrumentation invokedynamic instruction bootstrapping for instrumented class java.lang.ClassLoader and advice io.opentelemetry.javaagent.instrumentation.internal.classloader.LoadInjectedClassInstrumentation$LoadClassAdvice.onEnter, the instrumentaiton should be active now
[otel.javaagent 2024-10-24 12:42:55:463 +0200] [otel-javaagent-transform-safe-logger] DEBUG io.opentelemetry.javaagent.tooling.AgentInstaller$TransformLoggingListener - Failed to handle io.opentelemetry.javaagent.ClassLoaderData$$29539e36 for transformation on class loader io.opentelemetry.javaagent.tooling.util.ClassLoaderMap$1@29539e36
java.lang.StackOverflowError
	at io.opentelemetry.javaagent.instrumentation.internal.classloader.LoadInjectedClassInstrumentation$LoadClassAdvice.onEnter(LoadInjectedClassInstrumentation.java:59)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:352)
	at io.opentelemetry.javaagent.instrumentation.internal.classloader.LoadInjectedClassInstrumentation$LoadClassAdvice.onEnter(LoadInjectedClassInstrumentation.java:59)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:352)
	at io.opentelemetry.javaagent.instrumentation.internal.classloader.LoadInjectedClassInstrumentation$LoadClassAdvice.onEnter(LoadInjectedClassInstrumentation.java:59)
	...
	at java.lang.ClassLoader.loadClass(ClassLoader.java:352)
	at io.opentelemetry.javaagent.instrumentation.internal.classloader.LoadInjectedClassInstrumentation$LoadClassAdvice.onEnter(LoadInjectedClassInstrumentation.java:59)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:352)
	at io.opentelemetry.javaagent.instrumentation.internal.classloader.LoadInjectedClassInstrumentation$LoadClassAdvice.onEnter(LoadInjectedClassInstrumentation.java:59)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:352)
	at io.opentelemetry.javaagent.tooling.ignore.IgnoredClassLoadersMatcher.loadsExpectedClass(IgnoredClassLoadersMatcher.java:78)
	at io.opentelemetry.javaagent.tooling.ignore.IgnoredClassLoadersMatcher.delegatesToBootstrap(IgnoredClassLoadersMatcher.java:69)

If you prefer to, we could also use CallDepth in the LoadClassAdvice instead I think.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is fine as it is for now.

…ling/instrumentation/indy/InstrumentationModuleClassLoader.java
@trask trask enabled auto-merge (squash) October 24, 2024 22:56
@trask trask merged commit 427c8e5 into open-telemetry:main Oct 24, 2024
56 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants