-
Notifications
You must be signed in to change notification settings - Fork 14.1k
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
KAFKA-18211: Override class loaders for class graph scanning in connect. #18403
base: trunk
Are you sure you want to change the base?
Changes from 3 commits
581290b
3bffee7
986f222
2d41315
25d3f57
0d7e0b1
274b0b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ | |
|
||
package org.apache.kafka.connect.runtime.isolation; | ||
|
||
import org.apache.kafka.connect.json.JsonConverter; | ||
import org.junit.jupiter.api.io.TempDir; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
|
@@ -31,6 +32,7 @@ | |
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertFalse; | ||
import static org.junit.jupiter.api.Assertions.assertInstanceOf; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
|
||
|
@@ -137,6 +139,17 @@ public void testVersionedPluginsHasVersion(PluginScanner scanner) { | |
versionedPluginResult.forEach(pluginDesc -> assertEquals("1.0.0", pluginDesc.version())); | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("parameters") | ||
public void testClasspathPluginIsAlsoLoadedInIsolation(PluginScanner scanner) { | ||
// json converter is part of the classpath by default | ||
String jsonConverterLocation = JsonConverter.class.getProtectionDomain().getCodeSource().getLocation().getPath(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels very brittle to me. Rather than using a real class, can this use a TestPlugin with a similar name to a classpath plugin? Or the last time we needed to set up a "classpath" plugin, it was by injecting a new loader in the hierarchy in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated the test using a TestPlugin. Setting up a test plugin the way we do in |
||
PluginScanResult result = scan(scanner, Collections.singleton(Path.of(jsonConverterLocation))); | ||
assertFalse(result.converters().isEmpty()); | ||
result.converters().stream().filter(pluginDesc -> pluginDesc.className().equals(JsonConverter.class.getName())) | ||
.forEach(pluginDesc -> assertInstanceOf(PluginClassLoader.class, pluginDesc.loader())); | ||
} | ||
|
||
private PluginScanResult scan(PluginScanner scanner, Set<Path> pluginLocations) { | ||
ClassLoaderFactory factory = new ClassLoaderFactory(); | ||
Set<PluginSource> pluginSources = PluginUtils.pluginSources(pluginLocations, PluginScannerTest.class.getClassLoader(), factory); | ||
|
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.
Is this true? What is the
ignoreParentClassLoaders
method doing then?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.
I did some more analysis on this. My statement is somewhat incorrect. Classgraph does try to find and scan the classes from parent but it fails to do so. Elaborating more.
Classgraph scanning works by computing a list of class path URLs from the provided class loaders and then manually scans the
.class
files under the URLs to retrieve information about those classes. Unless we instruct classloading explicitly (which we do here) the classes are not loaded using Java's classloading. If the desired class is not found in the scanning our code never tries to load the class. The list of class path URLs are ordered based on the ordering of the provided classloaders. All the logic is in the constructor of ClasspathFinder.ClasspathFinder uses various ClassLoaderHandlers to obtain the set of URLs for a classloader. Connects PluginClassLoader uses URLClassLoaderHandler which works fine and gets the list of jars in a plugin path. But when it comes to the application classloader and platform classloader which has the URLs for classpath it uses JPMSClassLoaderHelper and tries to get the URLs through a illegal reflections access on a private field, which will fail on modern Java (throws an IllegalAccessException, can be mitigated using
--illegal-access=permit
but this is not present since Java 17 and not really recommended). Even though the classloader chain is computed the, URLs in classpath are not obtained because of this. This is why some of the tests were failing whereSubclassOfClasspathConverter
which extensByteArrayConverter
was not computed to be an implementation of a converter sinceByteArrayConverter
is in classpath.To force classpath URL scanning explicitly we need ClasspathFinder to execute this part of code, which is only possible with classloader overrides if one of the provided classloader is application/platform classloader. Passing the classloader chain for the PluginClassLoader achieves this.
ignoreParentClassLoader
is tied tooverrideClassloader == null
check, hence is of no use with classLoader overrides.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.
Okay, thanks for the explanation, I see that ClassGraph is going outside the classloader hierarchy for scanning, and just uses a list of jars. Here's what I was seeing:
Trunk
Single Loader
Loader and parents
It doesn't appear that the order in which we specify the classloaders has an effect on the order in which scanning actually takes place. And I think that makes sense given the ClassLoaderHandler stuff; It's applying a deterministic ordering to generate these lists of jars.
Look at this: https://github.com/classgraph/classgraph/blob/6f9012f2a193ebfefe4a4384e7642820e7aab0f5/src/main/java/nonapi/io/github/classgraph/classloaderhandler/README
This sounds like it applies to us, because we have a child-first/parent-last delegation order, but we don't have a special handler telling ClassGraph about it. Maybe we can pursue upstreaming a handler to make the ordering work properly.
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.
But I can see that your reproduction case fails with the old code and succeeds with the new code, why is that?
I can see after the initial
ClassGraph#scan()
call, it actually always finds the PluginClassLoader isolated copy of ByteArrayConverter, even with the trunk implementation. The difference happens inside ofClassInfoList#loadClasses
andClassGraphClassLoader#loadClass
. Here's what I was seeing there:Trunk
environmentClassLoaderDelegationOrder
Loader and parents
overrideClassLoaders
So the order of the specified classloaders does change the classloading order of the plugins, even though it doesn't change the scanning behavior.
I think the current implementation is satisfactory as a workaround for the behavior, but we should follow-up with ClassGraph and try and use their ClassLoaderHandler infrastructure to get the right classloader sorting. I explored circumventing the ClassGraphClassLoader entirely and calling
Class.forName(..., ..., source.loader())
, but its a bit clunky because we have to also include a bunch of error handling.