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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

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.

* Copy of {@link Constants#BOOTSTRAP_PACKAGE_PREFIXES} because the Constants class
* is not accessible from here
*/
public static final List<String> BOOTSTRAP_PACKAGE_PREFIXES =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test seemed to have relied on the side effect of Constants being injected into every ClassLoader containing ClassLoaders. We don't inject it anymore, which caused this test to fail for indy.

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>
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.

2 participants