Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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 @@ -10,11 +10,13 @@
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class ClassLoaderInstrumentationModule extends InstrumentationModule {
public class ClassLoaderInstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {
public ClassLoaderInstrumentationModule() {
super("internal-class-loader");
}
Expand All @@ -26,13 +28,24 @@ public boolean defaultEnabled(ConfigProperties config) {
}

@Override
public boolean isIndyModule() {
return false;
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");
}

@Override
public boolean isHelperClass(String className) {
return className.equals("io.opentelemetry.javaagent.tooling.Constants");
public boolean loadAdviceClassesEagerly() {
// This is required due to DefineClassInstrumentation
// Without this, we would get an infinite recursion on bootstrapping of that instrumentation:
// 1. ClassLoader.defineClass is invoked somewhere
// 2. The inserted invokedynamic for the instrumentation is reached
// 3. To bootstrap the advice, IndyBootstrap is invoked
// 4. IndyBootstrap tries to load the DefineClassInstrumentation Advice class
// 5. The loading calls ClassLoader.defineClass -> recursion, BOOM!
// We avoid this recursion by ensuring that the DefineClassInstrumentation Advice class
// is loaded eagerly before the corresponding invokedynamic is reached
return true;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

// not touch this advice
// this is done to ensure that ClassLoaderInstrumentationModule.loadAdviceClassesEagerly works
// correctly
// we can therefore remove it, as soon as the AdviceTransformer is not applied anymore
@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 +76,17 @@ 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 to ensure that ClassLoaderInstrumentationModule.loadAdviceClassesEagerly works
// correctly
// we can therefore remove it, as soon as the AdviceTransformer is not applied anymore
@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 @@ -8,6 +8,8 @@
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

Expand All @@ -32,7 +34,12 @@ public class ResourceInjectionInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return extendsClass(named("java.lang.ClassLoader"));
return extendsClass(named("java.lang.ClassLoader"))
.and(
not(
namedOneOf(
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader",
"io.opentelemetry.javaagent.tooling.instrumentation.indy.InstrumentationModuleClassLoader")));
}

@Override
Expand Down Expand Up @@ -61,37 +68,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 +115,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 @@ -142,6 +142,15 @@ private static int getJavaVersion() {
return Integer.parseInt(javaSpecVersion);
}

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// We explicitly override loadClass from ClassLoader to ensure
// that loadClass is properly excluded from our internal ClassLoader Instrumentations
// (e.g. LoadInjectedClassInstrumentation, BooDelegationInstrumentation)
// Otherwise this will cause recursion in invokedynamic linkage
return loadClass(name, false);
}

@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// ContextStorageOverride is meant for library instrumentation we don't want it to apply to our
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,19 @@ default String getModuleGroup() {
default List<String> agentPackagesToHide() {
return Collections.emptyList();
}

/**
* By default, Advice classes are loaded lazily the first time the corresponding instrumented
* method is invoked. This function allows to change the behaviour so that all Advice classes are
* loaded and initialized as soon as a matching type is instrumented.
*
* <p>Note: this functionality currently does not work together with the AdviceTransformer.
* Therefore you should make your Advices indy-compatible (use @Advice.AssignReturned) before
* using this feature.
*
* @return true, if Advice classes should be loaded on instrumentation instead of first execution
*/
default boolean loadAdviceClassesEagerly() {
return false;
}
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ public class IndyBootstrap {

IndyBootstrapDispatcher.init(
MethodHandles.lookup().findStatic(IndyBootstrap.class, "bootstrap", bootstrapMethodType));

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

} catch (Exception e) {
throw new IllegalStateException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import net.bytebuddy.agent.builder.AgentBuilder;
Expand Down Expand Up @@ -52,6 +54,9 @@
*/
public class InstrumentationModuleClassLoader extends ClassLoader {

private static final Logger logger =
Logger.getLogger(InstrumentationModuleClassLoader.class.getName());

static {
ClassLoader.registerAsParallelCapable();
}
Expand Down Expand Up @@ -150,8 +155,24 @@ public synchronized void installModule(InstrumentationModule module) {
className -> BytecodeWithUrl.create(className, agentOrExtensionCl)));
installInjectedClasses(classesToInject);
if (module instanceof ExperimentalInstrumentationModule) {
hiddenAgentPackages.addAll(
((ExperimentalInstrumentationModule) module).agentPackagesToHide());
ExperimentalInstrumentationModule experimentalModule =
(ExperimentalInstrumentationModule) module;
hiddenAgentPackages.addAll(experimentalModule.agentPackagesToHide());
if (experimentalModule.loadAdviceClassesEagerly()) {
for (String adviceClass : getModuleAdviceNames(module)) {
try {
this.loadClass(adviceClass, true);
} catch (ClassNotFoundException e) {
logger.log(
Level.SEVERE,
"Failed to eagerly load advice class {0}",
new Object[] {adviceClass, e});
}
}
// We also eagerly load the LookupExposer, because that is also required for invokedynamic
// bootstrapping
getLookup();
}
}
}

Expand Down Expand Up @@ -196,6 +217,15 @@ public void applyTransformer(AgentBuilder.Transformer transformer) {}

public static final Map<String, byte[]> bytecodeOverride = new ConcurrentHashMap<>();

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// We explicitly override loadClass from ClassLoader to ensure
// that loadClass is properly excluded from our internal ClassLoader Instrumentations
// (e.g. LoadInjectedClassInstrumentation, BooDelegationInstrumentation)
// Otherwise this will cause recursion in invokedynamic linkage
return loadClass(name, false);
}

@Override
@SuppressWarnings("removal") // AccessController is deprecated for removal
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Expand Down
1 change: 1 addition & 0 deletions testing-common/integration-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies {

testCompileOnly(project(":instrumentation-api"))
testCompileOnly(project(":javaagent-tooling"))
testCompileOnly(project(":javaagent-bootstrap"))
testCompileOnly(project(":javaagent-extension-api"))
testCompileOnly(project(":muzzle"))

Expand Down
Loading
Loading