-
Notifications
You must be signed in to change notification settings - Fork 325
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
Refactor Servlet API and JDBC plugins to use a MethodHandles-based advice dispatching approach #1090
Refactor Servlet API and JDBC plugins to use a MethodHandles-based advice dispatching approach #1090
Conversation
First step of elastic#1085
…ce-via-method-handle
…her class loader Remove unnecessary helpers from servlet instrumentation
Removed @VisibleForAdvice
- Helper CL unloading (GC) test - polishing
@Advice.OnMethodEnter(suppress = Throwable.class) | ||
private static Transaction onEnterServletService(@Advice.Origin Class<?> clazz, | ||
@Advice.Argument(0) ServletRequest servletRequest) throws Throwable { | ||
return (Transaction) MethodHandleDispatcher |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referencing agent types in advice methods might not be possible anymore once we load the agent from an isolated class loader.
Codecov Report
@@ Coverage Diff @@
## master #1090 +/- ##
=========================================
Coverage 58.52% 58.52%
Complexity 87 87
=========================================
Files 334 334
Lines 15753 15753
Branches 2253 2253
=========================================
Hits 9220 9220
Misses 5912 5912
Partials 621 621 Continue to review full report at Codecov.
|
…ce-via-method-handle
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really good! 👏
One suggestion of making it safer with postponing helper class resolution and a request for extending the class-leak tests.
Other than that - cosmetic stuff, mostly trying to make it easier to use right and understand.
Well done!
apm-agent-core/src/main/java/co/elastic/apm/agent/bci/HelperClassManager.java
Show resolved
Hide resolved
apm-agent-core/src/main/java/co/elastic/apm/agent/bci/HelperClassManager.java
Show resolved
Hide resolved
} | ||
|
||
@Nullable | ||
public static ConcurrentMap<String, MethodHandle> getDispatcherForClassLoader(@Nullable ClassLoader classLoader) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Terminology- in general, there are lots of mentions to "dispatcher", but there are no such entities. I suggest you either add such (probably instances of this class) that hold the method handle registries and these instances will be held by the MethodHandleDispatcherHolder
(as its name suggests), or you reference them here as registries (rather than "dispatcher") eg setMethodHandleRegistryForClassLoader
.
apm-agent-core/src/main/java/co/elastic/apm/agent/bci/HelperClassManager.java
Show resolved
Hide resolved
Class<?> clazz = Class.forName(name, true, helperCL); | ||
if (clazz.getClassLoader() != helperCL) { | ||
throw new IllegalStateException("Helper classes not loaded from helper class loader, instead loaded from " + clazz.getClassLoader()); | ||
} | ||
try { | ||
// call in from the context of the created helper class loader so that helperClass.getMethods() doesn't throw NoClassDefFoundError | ||
Method registerStaticMethodHandles = methodHandleRegistererClass.getMethod("registerStaticMethodHandles", ConcurrentMap.class, ConcurrentMap.class, Class.class); | ||
registerStaticMethodHandles.invoke(null, dispatcherForClassLoader, reflectionDispatcherForClassLoader, clazz); | ||
} catch (Exception e) { | ||
logger.error("Exception while trying to register method handles for {}", name); | ||
if (e instanceof InvocationTargetException) { | ||
Throwable targetException = ((InvocationTargetException) e).getTargetException(); | ||
if (targetException instanceof Exception) { | ||
throw (Exception) targetException; | ||
} | ||
} | ||
throw e; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part may be done prematurely - it will be invoked when the bytecode-injection candidate class is found, however the helper may depend on other classes that are not yet loaded and loading them here is a side effect. The use of reflection means completion of the helper classes linkage, which kind of guarantees that. Related errors will be very hard to detect.
I think that this part should be done lazily, meaning - keep a mapping between the helper class name and the helperCL and do the actual registration only upon first invocation of the injected method. Should be a lot safer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lazily injecting the helper classes has the downside that we wouldn't be able to leverage an upcoming feature in Byte Buddy. This feature would inject an INVOKEDYNAMIC
instruction to call the helper method: raphw/byte-buddy#830 (comment)
This avoids the overhead of having to look up the method handle from a map.
However, there are a few things we can do to make this safer:
- We already only load classes where we checked with Byte Buddy matchers that they contain
@RegisterMethodHandle
-annotated methods. - Call
getMethods
instead ofgetDeclaredMethods
: avoids resolving types of all parameters of non-public methods. So we could make methods with problematic signatures private or package-private. - Using the upcoming
bootstrap
method, I think we'd be able to get the method handles without having to use reflection. We'd need to do some testing but I suspect we'll need a fallback for Java 7 that does not rely onINVOKEDYNAMIC
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lazily injecting the helper classes has the downside that we wouldn't be able to leverage an upcoming feature in Byte Buddy
Is it something we should rely on? And won't we have to do some adjustments anyhow?
Call getMethods instead of getDeclaredMethods: avoids resolving types of all parameters of non-public methods. So we could make methods with problematic signatures private or package-private.
The nature of these problems is that they are very setup-dependant, so there are not "problematic signatures" per se. In most cases it will work, but somewhere (eg some module-dependency systems) these side effects may cause a lookup of a type that is not yet available.
Bottom line- it is safer and if the effort is not big, I think it worth it (as the effort to debug even a single related problem could be bigger). However, if the effort is substantial, or if you think it will require a rework effort in the foreseeable future - let's merge as is and rethink later if needed. It's really your call, either way is fine for me.
import java.util.WeakHashMap; | ||
import java.util.concurrent.atomic.AtomicInteger; | ||
|
||
// TODO docs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do docs
apm-agent-core/src/test/java/co/elastic/apm/agent/util/PackageScannerTest.java
Show resolved
Hide resolved
} | ||
@Override | ||
public List<String> helpers() throws Exception { | ||
return PackageScanner.getClassNames(getClass().getPackage().getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not be more specific (only real helper classes)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My vision for an ideal situation (which requires some changes in Byte Buddy) is that advices can be written without having to manually get a MethodHandle
. When we load all methods from the helper class loader, you can even implement an instrumentation with just one file, without having to worry about which classes are able to access application classes.
After all plugins have been refactored, we can pull this method up, making it the default behavior to include all classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can even implement an instrumentation with just one file
In general, a separation will reduce the chances for errors. It's always better if you have a class exactly once in your classpath. Consider such an instrumentation class with a static field, or some initialization code run in the instrumentation constructor. What can actually be seen in runtime from these annotated method is very difficult to understand when just looking at such file.
Again- not a blocker.
apm-agent-core/src/main/java/co/elastic/apm/agent/util/PackageScanner.java
Show resolved
Hide resolved
@@ -40,6 +42,11 @@ | |||
return Collections.singleton(SERVLET_API); | |||
} | |||
|
|||
@Override | |||
public List<String> helpers() throws Exception { | |||
return PackageScanner.getClassNames(getClass().getPackage().getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same- since this would be a reference for future usages, I think it's better to extract the @RegisterMethodHandle
methods to dedicated classes in dedicated packages. It's easier to make mistakes if we mix instrumentations and advices with those. It also makes sense that packages reflect the complicated class loading contexts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be merged as is if all tests work. I would run some manual tests in addition in specific scenarios, eg using the attach methods as well, or JDBC on real servlet container.
superseded by #1230 |
First step of #1085