Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -34,20 +34,6 @@ public boolean isHelperClass(String className) {
return className.equals("io.opentelemetry.javaagent.tooling.Constants");
}

@Override
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
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ public static DefineClassContext onEnter(

// 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
// 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)
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
Expand All @@ -78,9 +76,7 @@ public static DefineClassContext onEnter(

// 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
// 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)
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static CallSite bootstrap(
}

// 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
Expand Up @@ -61,19 +61,4 @@ 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
@@ -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 @@ -5,14 +5,14 @@

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

import io.opentelemetry.javaagent.bootstrap.CallDepth;
import io.opentelemetry.javaagent.bootstrap.IndyBootstrapDispatcher;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
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.invoke.MutableCallSite;
import java.lang.reflect.Method;
import java.security.PrivilegedAction;
import java.util.Arrays;
Expand Down Expand Up @@ -84,7 +84,7 @@ public class IndyBootstrap {

MethodType bootstrapMethodType =
MethodType.methodType(
ConstantCallSite.class,
CallSite.class,
MethodHandles.Lookup.class,
String.class,
MethodType.class,
Expand All @@ -93,11 +93,7 @@ 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());
AdviceBootstrapState.initialize();
} catch (Exception e) {
throw new IllegalStateException(e);
}
Expand All @@ -111,7 +107,7 @@ public static Method getIndyBootstrapMethod() {

@Nullable
@SuppressWarnings({"unused", "removal"}) // SecurityManager and AccessController are deprecated
private static ConstantCallSite bootstrap(
private static CallSite bootstrap(
MethodHandles.Lookup lookup,
String adviceMethodName,
MethodType adviceMethodType,
Expand All @@ -124,11 +120,11 @@ private static ConstantCallSite bootstrap(
// callsite resolution needs privileged access to call Class#getClassLoader() and
// MethodHandles$Lookup#findStatic
return java.security.AccessController.doPrivileged(
(PrivilegedAction<ConstantCallSite>)
(PrivilegedAction<CallSite>)
() -> internalBootstrap(lookup, adviceMethodName, adviceMethodType, args));
}

private static ConstantCallSite internalBootstrap(
private static CallSite internalBootstrap(
MethodHandles.Lookup lookup,
String adviceMethodName,
MethodType adviceMethodType,
Expand Down Expand Up @@ -163,29 +159,33 @@ private static ConstantCallSite internalBootstrap(
}
}

private static ConstantCallSite bootstrapAdvice(
private static CallSite bootstrapAdvice(
MethodHandles.Lookup lookup,
String adviceMethodName,
MethodType invokedynamicMethodType,
String moduleClassName,
String adviceMethodDescriptor,
String adviceClassName)
throws NoSuchMethodException, IllegalAccessException, ClassNotFoundException {
CallDepth callDepth = CallDepth.forClass(IndyBootstrap.class);
try {
if (callDepth.getAndIncrement() > 0) {
try (AdviceBootstrapState nestedState =
AdviceBootstrapState.enter(
lookup.lookupClass(),
moduleClassName,
adviceClassName,
adviceMethodName,
adviceMethodDescriptor)) {
if (nestedState.isNestedInvocation()) {
// avoid re-entrancy and stack overflow errors, which may happen when bootstrapping an
// instrumentation that also gets triggered during the bootstrap
// for example, adding correlation ids to the thread context when executing logger.debug.
logger.log(
Level.SEVERE,
"Nested instrumented invokedynamic instruction linkage detected for instrumented class {0} and advice {1}.{2}, the instrumentation might not work correctly",
new Object[] {lookup.lookupClass().getName(), adviceClassName, adviceMethodName});
logger.log(
Level.SEVERE,
"Stacktrace for nested invokedynamic instruction linkage:",
new Throwable());
return null;
MutableCallSite mutableCallSite = nestedState.getMutableCallSite();
if (mutableCallSite == null) {
mutableCallSite =
new MutableCallSite(
IndyBootstrapDispatcher.generateNoopMethodHandle(invokedynamicMethodType));
nestedState.initMutableCallSite(mutableCallSite);
}
return mutableCallSite;
}

InstrumentationModuleClassLoader instrumentationClassloader =
Expand All @@ -203,9 +203,21 @@ private static ConstantCallSite bootstrapAdvice(
.getLookup()
.findStatic(adviceClass, adviceMethodName, actualAdviceMethodType)
.asType(invokedynamicMethodType);
return new ConstantCallSite(methodHandle);
} finally {
callDepth.decrementAndGet();

MutableCallSite nestedBootstrapCallSite = nestedState.getMutableCallSite();
if (nestedBootstrapCallSite != null) {
// There have been nested bootstrapping attempts
// Update the callsite of those to run the actual instrumentation
logger.log(
Level.FINE,
"Fixing nested instrumentation invokedynamic instruction bootstrapping for instrumented class {0} and advice {1}.{2}, the instrumentaiton should be active now",
new Object[] {lookup.lookupClass().getName(), adviceClassName, adviceMethodName});
nestedBootstrapCallSite.setTarget(methodHandle);
MutableCallSite.syncAll(new MutableCallSite[] {nestedBootstrapCallSite});
return nestedBootstrapCallSite;
} else {
return new ConstantCallSite(methodHandle);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
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 @@ -54,9 +52,6 @@
*/
public class InstrumentationModuleClassLoader extends ClassLoader {

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

static {
ClassLoader.registerAsParallelCapable();
}
Expand Down Expand Up @@ -158,21 +153,6 @@ public synchronized void installModule(InstrumentationModule module) {
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
Loading