Skip to content

Commit

Permalink
Load stable plugins as synthetic modules (#91869)
Browse files Browse the repository at this point in the history
* Load nonmodular stable plugins as ubermodules

We can use the ubermodule classloader to load stable plugins if the
plugin descriptor does not give us a module to load from the plugin
bundle.

We create the synthetic module name by munging the plugin name into a
suitable form.

For testing, we have to provide the uber module classloader read access to the app classloader's unnamed module, where the stable api modules are loaded by default.

* Update docs/changelog/91869.yaml
  • Loading branch information
williamrandolph authored Nov 29, 2022
1 parent 4026ff2 commit 788750b
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/91869.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 91869
summary: Load stable plugins as synthetic modules
area: Infra/Plugins
type: enhancement
issues: []
21 changes: 21 additions & 0 deletions server/src/main/java/org/elasticsearch/plugins/PluginsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -523,12 +523,33 @@ static LayerAndLoader createPlugin(
extendedPlugins.stream().map(LoadedPlugin::layer)
).toList();
return createPluginModuleLayer(bundle, pluginParentLoader, parentLayers);
} else if (plugin.isStable()) {
logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular as synthetic module");
return LayerAndLoader.ofLoader(
UberModuleClassLoader.getInstance(
pluginParentLoader,
ModuleLayer.boot(),
"synthetic." + toModuleName(plugin.getName()),
bundle.allUrls,
Set.of("org.elasticsearch.server") // TODO: instead of denying server, allow only jvm + stable API modules
)
);
} else {
logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular");
return LayerAndLoader.ofLoader(URLClassLoader.newInstance(bundle.urls.toArray(URL[]::new), pluginParentLoader));
}
}

// package-visible for testing
static String toModuleName(String name) {
String result = name.replaceAll("\\W+", ".") // replace non-alphanumeric character strings with dots
.replaceAll("(^[^A-Za-z_]*)", "") // trim non-alpha or underscore characters from start
.replaceAll("\\.$", "") // trim trailing dot
.toLowerCase(Locale.getDefault());
assert ModuleSupport.isPackageName(result);
return result;
}

private static void checkDeprecations(
PluginIntrospector inspector,
List<PluginDescriptor> pluginDescriptors,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ protected Class<?> loadClass(String cn, boolean resolve) throws ClassNotFoundExc
}
}

// For testing in cases where code must be given access to an unnamed module
void addReadsSystemClassLoaderUnnamedModule() {
moduleController.layer()
.modules()
.forEach(module -> moduleController.addReads(module, ClassLoader.getSystemClassLoader().getUnnamedModule()));
}

/**
* Returns the package name for the given class name
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.index.IndexModule;
import org.elasticsearch.plugin.analysis.api.CharFilterFactory;
import org.elasticsearch.plugins.scanners.PluginInfo;
import org.elasticsearch.plugins.spi.BarPlugin;
import org.elasticsearch.plugins.spi.BarTestService;
import org.elasticsearch.plugins.spi.TestService;
Expand Down Expand Up @@ -823,6 +825,23 @@ public Reader create(Reader reader) {
assertThat(pluginInfos.get(0).descriptor().getName(), equalTo("stable-plugin"));
assertThat(pluginInfos.get(0).descriptor().isStable(), is(true));

// check ubermodule classloader usage
Collection<PluginInfo> stablePluginInfos = pluginService.getStablePluginRegistry()
.getPluginInfosForExtensible("org.elasticsearch.plugin.analysis.api.CharFilterFactory");
assertThat(stablePluginInfos, hasSize(1));
ClassLoader stablePluginClassLoader = stablePluginInfos.stream().findFirst().orElseThrow().loader();
assertThat(stablePluginClassLoader, instanceOf(UberModuleClassLoader.class));

if (CharFilterFactory.class.getModule().isNamed() == false) {
// test frameworks run with stable api classes on classpath, so we
// have no choice but to let our class read the unnamed module that
// owns the stable api classes
((UberModuleClassLoader) stablePluginClassLoader).addReadsSystemClassLoaderUnnamedModule();
}

Class<?> stableClass = stablePluginClassLoader.loadClass("p.A");
assertThat(stableClass.getModule().getName(), equalTo("synthetic.stable.plugin"));

// TODO should we add something to pluginInfos.get(0).pluginApiInfo() ?
} finally {
closePluginLoaders(pluginService);
Expand All @@ -838,6 +857,20 @@ public void testCanCreateAClassLoader() {
assertEquals(this.getClass().getClassLoader(), loader.getParent());
}

public void testToModuleName() {
assertThat(PluginsService.toModuleName("module.name"), equalTo("module.name"));
assertThat(PluginsService.toModuleName("module-name"), equalTo("module.name"));
assertThat(PluginsService.toModuleName("module-name1"), equalTo("module.name1"));
assertThat(PluginsService.toModuleName("1module-name"), equalTo("module.name"));
assertThat(PluginsService.toModuleName("module-name!"), equalTo("module.name"));
assertThat(PluginsService.toModuleName("module!@#name!"), equalTo("module.name"));
assertThat(PluginsService.toModuleName("!module-name!"), equalTo("module.name"));
assertThat(PluginsService.toModuleName("module_name"), equalTo("module_name"));
assertThat(PluginsService.toModuleName("-module-name-"), equalTo("module.name"));
assertThat(PluginsService.toModuleName("_module_name"), equalTo("_module_name"));
assertThat(PluginsService.toModuleName("_"), equalTo("_"));
}

static final class Loader extends ClassLoader {
Loader(ClassLoader parent) {
super(parent);
Expand Down

0 comments on commit 788750b

Please sign in to comment.