-
Notifications
You must be signed in to change notification settings - Fork 25.6k
[WIP] Non-modular plugins with synthetic module #117570
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
Changes from 6 commits
ed6d1d8
e5e5ce5
4c3f232
bdcdb30
d188bec
374731a
2eeb099
a9a768c
241a5b5
c3c0127
2ad48c9
daf710c
7933ada
8707701
56c050e
298405e
a5594ea
a4d39de
0f56ab9
990b04d
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 |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ | |
| import java.security.PrivilegedAction; | ||
| import java.security.SecureClassLoader; | ||
| import java.util.Enumeration; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
@@ -58,43 +59,66 @@ public class UberModuleClassLoader extends SecureClassLoader implements AutoClos | |
| private final ModuleLayer.Controller moduleController; | ||
| private final Set<String> packageNames; | ||
|
|
||
| private static Map<String, Set<String>> getModuleToServiceMap(ModuleLayer moduleLayer) { | ||
| Set<String> unqualifiedExports = moduleLayer.modules() | ||
| private static Set<String> unqualifiedExportsForLayer(ModuleLayer layer) { | ||
| return layer.modules() | ||
| .stream() | ||
| .flatMap(module -> module.getDescriptor().exports().stream()) | ||
| .filter(Predicate.not(ModuleDescriptor.Exports::isQualified)) | ||
| .map(ModuleDescriptor.Exports::source) | ||
| .collect(Collectors.toSet()); | ||
| return moduleLayer.modules() | ||
| } | ||
|
|
||
| private static Set<ModuleDescriptor> readableModulesForLayer(ModuleLayer layer) { | ||
| return layer.modules() | ||
| .stream() | ||
| .map(Module::getDescriptor) | ||
| .filter(ModuleSupport::hasAtLeastOneUnqualifiedExport) | ||
| .filter(md -> ModuleSupport.hasAtLeastOneUnqualifiedExport(md) || md.isOpen() || md.isAutomatic()) | ||
| .collect(Collectors.toSet()); | ||
| } | ||
|
|
||
| private static Map<String, Set<String>> getModuleToServiceMap(List<ModuleLayer> moduleLayers) { | ||
| Set<String> allUnqualifiedExports = new HashSet<>(); | ||
| Set<ModuleDescriptor> allReadableModules = new HashSet<>(); | ||
| for (var layer : moduleLayers) { | ||
| allUnqualifiedExports.addAll(unqualifiedExportsForLayer(layer)); | ||
| allReadableModules.addAll(readableModulesForLayer(layer)); | ||
| for (var parentLayer : layer.parents()) { | ||
| // ModuleLayer.boot() contains all ES libs too, so we want it to be an explicit parent | ||
| if (parentLayer != ModuleLayer.boot()) { | ||
| allUnqualifiedExports.addAll(unqualifiedExportsForLayer(parentLayer)); | ||
| allReadableModules.addAll(readableModulesForLayer(parentLayer)); | ||
| // TODO: recurse? | ||
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| return allReadableModules.stream() | ||
| .collect( | ||
| Collectors.toMap( | ||
| ModuleDescriptor::name, | ||
| md -> md.provides() | ||
| .stream() | ||
| .map(ModuleDescriptor.Provides::service) | ||
| .filter(name -> unqualifiedExports.contains(packageName(name))) | ||
| .filter(name -> allUnqualifiedExports.contains(packageName(name))) | ||
| .collect(Collectors.toSet()) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| static UberModuleClassLoader getInstance(ClassLoader parent, String moduleName, Set<URL> jarUrls) { | ||
| return getInstance(parent, ModuleLayer.boot(), moduleName, jarUrls, Set.of()); | ||
| return getInstance(parent, List.of(ModuleLayer.boot()), moduleName, jarUrls, Set.of()); | ||
| } | ||
|
|
||
| @SuppressWarnings("removal") | ||
| static UberModuleClassLoader getInstance( | ||
| ClassLoader parent, | ||
| ModuleLayer parentLayer, | ||
| List<ModuleLayer> parentLayers, | ||
| String moduleName, | ||
| Set<URL> jarUrls, | ||
| Set<String> moduleDenyList | ||
| ) { | ||
| Path[] jarPaths = jarUrls.stream().map(UberModuleClassLoader::urlToPathUnchecked).toArray(Path[]::new); | ||
| var parentLayerModuleToServiceMap = getModuleToServiceMap(parentLayer); | ||
| var parentLayerModuleToServiceMap = getModuleToServiceMap(parentLayers); | ||
| Set<String> requires = parentLayerModuleToServiceMap.keySet() | ||
| .stream() | ||
| .filter(Predicate.not(moduleDenyList::contains)) | ||
|
|
@@ -110,10 +134,15 @@ static UberModuleClassLoader getInstance( | |
| jarPaths, | ||
| requires, | ||
| uses, | ||
| s -> isPackageInLayers(s, parentLayer) | ||
| s -> isPackageInLayers(s, parentLayers) | ||
| ); | ||
| // TODO: check that denied modules are not brought as transitive dependencies (or switch to allow-list?) | ||
| Configuration cf = parentLayer.configuration().resolve(finder, ModuleFinder.of(), Set.of(moduleName)); | ||
| Configuration cf = Configuration.resolve( | ||
| finder, | ||
| parentLayers.stream().map(ModuleLayer::configuration).toList(), | ||
| ModuleFinder.of(), | ||
| Set.of(moduleName) | ||
| ); | ||
|
|
||
| Set<String> packageNames = finder.find(moduleName).map(ModuleReference::descriptor).map(ModuleDescriptor::packages).orElseThrow(); | ||
|
|
||
|
|
@@ -122,20 +151,25 @@ static UberModuleClassLoader getInstance( | |
| moduleName, | ||
| jarUrls.toArray(new URL[0]), | ||
| cf, | ||
| parentLayer, | ||
| parentLayers, | ||
| packageNames | ||
| ); | ||
| return AccessController.doPrivileged(pa); | ||
| } | ||
|
|
||
| private static boolean isPackageInLayers(String packageName, ModuleLayer moduleLayer) { | ||
| if (moduleLayer.modules().stream().map(Module::getPackages).anyMatch(p -> p.contains(packageName))) { | ||
| private static boolean isPackageInLayers(String packageName, List<ModuleLayer> moduleLayers) { | ||
| if (moduleLayers.stream().flatMap(x -> x.modules().stream()).map(Module::getPackages).anyMatch(p -> p.contains(packageName))) { | ||
| return true; | ||
| } | ||
| if (moduleLayer.parents().equals(List.of(ModuleLayer.empty()))) { | ||
| return false; | ||
| for (var moduleLayer : moduleLayers) { | ||
| if (moduleLayer.parents().equals(List.of(ModuleLayer.empty()))) { | ||
| continue; | ||
| } | ||
| if (moduleLayer.parents().stream().anyMatch(ml -> isPackageInLayers(packageName, List.of(ml)))) { | ||
| return true; | ||
| } | ||
| } | ||
| return moduleLayer.parents().stream().anyMatch(ml -> isPackageInLayers(packageName, ml)); | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -146,7 +180,7 @@ private UberModuleClassLoader( | |
| String moduleName, | ||
| URL[] jarURLs, | ||
| Configuration cf, | ||
| ModuleLayer mparent, | ||
| List<ModuleLayer> mparent, | ||
| Set<String> packageNames | ||
| ) { | ||
| super(parent); | ||
|
|
@@ -157,7 +191,7 @@ private UberModuleClassLoader( | |
| // Defining a module layer tells the Java virtual machine about the | ||
| // classes that may be loaded from the module, and is what makes the | ||
| // Class::getModule call return the name of our ubermodule. | ||
| this.moduleController = ModuleLayer.defineModules(cf, List.of(mparent), s -> this); | ||
| this.moduleController = ModuleLayer.defineModules(cf, mparent, s -> this); | ||
| this.module = this.moduleController.layer().findModule(moduleName).orElseThrow(); | ||
|
|
||
| this.packageNames = packageNames; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,7 +30,8 @@ public class RestJvmCrashAction implements RestHandler { | |
| static { | ||
| try { | ||
| AccessController.doPrivileged((PrivilegedExceptionAction<?>) () -> { | ||
| Class<?> unsafe = Class.forName("sun.misc.Unsafe"); | ||
| // Bypass UberModuleClassLoader, as server does not have our very dangerous permissions | ||
|
Contributor
Author
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 shows an interesting side effect of using UberModuleClassLoader: for class loading, the server module/ProtectionDomain gets in the way. So if it does not have permissions (like |
||
| Class<?> unsafe = ClassLoader.getSystemClassLoader().loadClass("sun.misc.Unsafe"); | ||
|
|
||
| FREE_MEMORY = unsafe.getMethod("freeMemory", long.class); | ||
| Field f = unsafe.getDeclaredField("theUnsafe"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| module org.elasticsearch.spatial { | ||
|
Contributor
Author
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 should go in a separate PR |
||
| requires org.apache.lucene.spatial3d; | ||
| requires org.elasticsearch.h3; | ||
| requires org.elasticsearch.legacy.geo; | ||
| requires org.elasticsearch.painless.spi; | ||
| requires org.elasticsearch.xcore; | ||
| requires org.elasticsearch.server; | ||
| requires org.elasticsearch.xcontent; | ||
| requires org.elasticsearch.base; | ||
| requires org.elasticsearch.geo; | ||
| requires org.apache.lucene.core; | ||
|
|
||
| exports org.elasticsearch.xpack.spatial.action to org.elasticsearch.server; | ||
| exports org.elasticsearch.xpack.spatial.common; // to vector-tile | ||
| exports org.elasticsearch.xpack.spatial; // to vector-tile | ||
| exports org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid; // to vector-tile | ||
|
|
||
| exports org.elasticsearch.xpack.spatial.index.query; | ||
| exports org.elasticsearch.xpack.spatial.index.fielddata; | ||
| exports org.elasticsearch.xpack.spatial.index.fielddata.plain; | ||
| exports org.elasticsearch.xpack.spatial.index.mapper; | ||
|
|
||
| exports org.elasticsearch.xpack.spatial.ingest; | ||
| exports org.elasticsearch.xpack.spatial.script.field; | ||
|
|
||
| exports org.elasticsearch.xpack.spatial.search.aggregations; | ||
| exports org.elasticsearch.xpack.spatial.search.aggregations.metrics; | ||
| exports org.elasticsearch.xpack.spatial.search.aggregations.support; | ||
| exports org.elasticsearch.xpack.spatial.search.runtime; | ||
|
|
||
| opens org.elasticsearch.xpack.spatial to org.elasticsearch.painless.spi; // whitelist resource access | ||
|
|
||
| provides org.elasticsearch.painless.spi.PainlessExtension with org.elasticsearch.xpack.spatial.SpatialPainlessExtension; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ esplugin { | |
| name = 'x-pack-sql' | ||
| description 'The Elasticsearch plugin that powers SQL for Elasticsearch' | ||
| classname = 'org.elasticsearch.xpack.sql.plugin.SqlPlugin' | ||
| extendedPlugins = ['x-pack-ql', 'lang-painless'] | ||
| extendedPlugins = ['x-pack-ql', 'lang-painless', 'x-pack-core'] | ||
|
Contributor
Author
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 change should no longer be needed (I added recurse to parent layers/dependencies for 1 level), I'll revert this. |
||
| } | ||
|
|
||
| ext { | ||
|
|
||
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.
Very hacky, but I did not want to break the existing name schema for stable plugins.
And there is a
ml-package-loaderloader unfortunately --ml.package.loaderis not a OK module name, aspackageis a reserved keyword.