Skip to content

Commit e144722

Browse files
Handle providers of optional services in ubermodule classloader (#91217)
* When scanning a jar bundle for services to add to the ubermodule descriptor, we must make sure that any optional dependencies match up to services that are defined in the code. If we naively declare that our module uses any service that is implemented by one of its jars, we might fail to load the plugin because the original implementation is not included in our bundle. Here we add a fairly detailed test scenario for required and optional services defined outside the bundle, as well as for services defined within the bundle, either by module declaration or by META-INF/services entry.
1 parent 143828e commit e144722

File tree

3 files changed

+408
-36
lines changed

3 files changed

+408
-36
lines changed

server/src/main/java/org/elasticsearch/plugins/ModuleSupport.java

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,27 +45,41 @@ private ModuleSupport() {
4545
throw new AssertionError("Utility class, should not be instantiated");
4646
}
4747

48-
static ModuleFinder ofSyntheticPluginModule(String name, Path[] jarPaths, Set<String> requires, Set<String> uses) {
48+
static ModuleFinder ofSyntheticPluginModule(
49+
String name,
50+
Path[] jarPaths,
51+
Set<String> requires,
52+
Set<String> uses,
53+
Predicate<String> isPackageInParentLayers
54+
) {
4955
try {
5056
return new InMemoryModuleFinder(
51-
new InMemoryModuleReference(createModuleDescriptor(name, jarPaths, requires, uses), URI.create("module:/" + name))
57+
new InMemoryModuleReference(
58+
createModuleDescriptor(name, jarPaths, requires, uses, isPackageInParentLayers),
59+
URI.create("module:/" + name)
60+
)
5261
);
5362
} catch (IOException e) {
5463
throw new UncheckedIOException(e);
5564
}
5665
}
5766

5867
@SuppressForbidden(reason = "need access to the jar file")
59-
static ModuleDescriptor createModuleDescriptor(String name, Path[] jarPaths, Set<String> requires, Set<String> uses)
60-
throws IOException {
68+
static ModuleDescriptor createModuleDescriptor(
69+
String name,
70+
Path[] jarPaths,
71+
Set<String> requires,
72+
Set<String> uses,
73+
Predicate<String> isPackageInParentLayers
74+
) throws IOException {
6175
var builder = ModuleDescriptor.newOpenModule(name); // open module, for now
6276
requires.stream().forEach(builder::requires);
6377
uses.stream().forEach(builder::uses);
6478

6579
// scan the names of the entries in the JARs
6680
Set<String> pkgs = new HashSet<>();
6781
Map<String, List<String>> allBundledProviders = new HashMap<>();
68-
Set<String> allBundledServices = new HashSet<>();
82+
Set<String> servicesUsedInBundle = new HashSet<>();
6983
for (Path path : jarPaths) {
7084
assert path.getFileName().toString().endsWith(".jar") : "expected jars suffix, in path: " + path;
7185
try (JarFile jf = new JarFile(path.toFile(), true, ZipFile.OPEN_READ, Runtime.version())) {
@@ -74,13 +88,13 @@ static ModuleDescriptor createModuleDescriptor(String name, Path[] jarPaths, Set
7488
if (moduleInfo != null) {
7589
var descriptor = getDescriptorForModularJar(path);
7690
pkgs.addAll(descriptor.packages());
77-
allBundledServices.addAll(descriptor.uses());
91+
servicesUsedInBundle.addAll(descriptor.uses());
7892
for (ModuleDescriptor.Provides p : descriptor.provides()) {
7993
String serviceName = p.service();
8094
List<String> providersInModule = p.providers();
8195

8296
allBundledProviders.compute(serviceName, (k, v) -> createListOrAppend(v, providersInModule));
83-
allBundledServices.add(serviceName);
97+
servicesUsedInBundle.add(serviceName);
8498
}
8599
} else {
86100
var scan = scan(jf);
@@ -92,21 +106,29 @@ static ModuleDescriptor createModuleDescriptor(String name, Path[] jarPaths, Set
92106
List<String> providersInJar = getProvidersFromServiceFile(jf, serviceFileName);
93107

94108
allBundledProviders.compute(serviceName, (k, v) -> createListOrAppend(v, providersInJar));
95-
allBundledServices.add(serviceName);
109+
servicesUsedInBundle.add(serviceName);
96110
}
97111
}
98112
}
99113
}
100114

101115
builder.packages(pkgs);
102116

103-
// the module needs to use all services it provides, for the case of internal use
104-
allBundledServices.addAll(allBundledProviders.keySet());
105-
// but we don't want to add any services we already got from the parent layer
106-
allBundledServices.removeAll(uses);
117+
// we don't want to add any services we already got from the parent layer
118+
servicesUsedInBundle.removeAll(uses);
107119

108-
allBundledServices.forEach(builder::uses);
109-
allBundledProviders.forEach(builder::provides);
120+
// Services that aren't exported in the parent layer or defined in our
121+
// bundle. This can happen for optional (compile-time) dependencies
122+
Set<String> missingServices = servicesUsedInBundle.stream()
123+
.filter(s -> isPackageInParentLayers.test(toPackageName(s, ".").orElseThrow()) == false)
124+
.filter(s -> pkgs.contains(toPackageName(s, ".").orElseThrow()) == false)
125+
.collect(Collectors.toSet());
126+
127+
servicesUsedInBundle.stream().filter(s -> missingServices.contains(s) == false).forEach(builder::uses);
128+
allBundledProviders.entrySet()
129+
.stream()
130+
.filter(e -> missingServices.contains(e.getKey()) == false)
131+
.forEach(e -> builder.provides(e.getKey(), e.getValue()));
110132
return builder.build();
111133
}
112134

@@ -246,7 +268,7 @@ static boolean isJavaPlatformModule(ModuleDescriptor md) {
246268
@SuppressForbidden(reason = "need access to the jar file")
247269
private static List<String> getProvidersFromServiceFile(JarFile jf, String sf) throws IOException {
248270
try (BufferedReader bf = new BufferedReader(new InputStreamReader(jf.getInputStream(jf.getEntry(sf)), StandardCharsets.UTF_8))) {
249-
return bf.lines().toList();
271+
return bf.lines().filter(Predicate.not(l -> l.startsWith("#"))).filter(Predicate.not(String::isEmpty)).toList();
250272
}
251273
}
252274

server/src/main/java/org/elasticsearch/plugins/UberModuleClassLoader.java

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,16 @@ public class UberModuleClassLoader extends SecureClassLoader implements AutoClos
5757
private final ModuleLayer.Controller moduleController;
5858
private final Set<String> packageNames;
5959

60-
private static final Map<String, Set<String>> platformModulesToServices;
61-
62-
static {
63-
Set<String> unqualifiedExports = ModuleLayer.boot()
64-
.modules()
60+
private static Map<String, Set<String>> getModuleToServiceMap(ModuleLayer moduleLayer) {
61+
Set<String> unqualifiedExports = moduleLayer.modules()
6562
.stream()
6663
.flatMap(module -> module.getDescriptor().exports().stream())
6764
.filter(Predicate.not(ModuleDescriptor.Exports::isQualified))
6865
.map(ModuleDescriptor.Exports::source)
6966
.collect(Collectors.toSet());
70-
platformModulesToServices = ModuleLayer.boot()
71-
.modules()
67+
return moduleLayer.modules()
7268
.stream()
7369
.map(Module::getDescriptor)
74-
.filter(ModuleSupport::isJavaPlatformModule)
7570
.filter(ModuleSupport::hasAtLeastOneUnqualifiedExport)
7671
.collect(
7772
Collectors.toMap(
@@ -86,27 +81,38 @@ public class UberModuleClassLoader extends SecureClassLoader implements AutoClos
8681
}
8782

8883
static UberModuleClassLoader getInstance(ClassLoader parent, String moduleName, Set<URL> jarUrls) {
89-
return getInstance(parent, moduleName, jarUrls, Set.of());
84+
return getInstance(parent, ModuleLayer.boot(), moduleName, jarUrls, Set.of());
9085
}
9186

9287
@SuppressWarnings("removal")
93-
static UberModuleClassLoader getInstance(ClassLoader parent, String moduleName, Set<URL> jarUrls, Set<String> moduleDenyList) {
88+
static UberModuleClassLoader getInstance(
89+
ClassLoader parent,
90+
ModuleLayer parentLayer,
91+
String moduleName,
92+
Set<URL> jarUrls,
93+
Set<String> moduleDenyList
94+
) {
9495
Path[] jarPaths = jarUrls.stream().map(UberModuleClassLoader::urlToPathUnchecked).toArray(Path[]::new);
95-
96-
Set<String> requires = platformModulesToServices.keySet()
96+
var parentLayerModuleToServiceMap = getModuleToServiceMap(parentLayer);
97+
Set<String> requires = parentLayerModuleToServiceMap.keySet()
9798
.stream()
9899
.filter(Predicate.not(moduleDenyList::contains))
99100
.collect(Collectors.toSet());
100-
Set<String> uses = platformModulesToServices.entrySet()
101+
Set<String> uses = parentLayerModuleToServiceMap.entrySet()
101102
.stream()
102103
.filter(Predicate.not(entry -> moduleDenyList.contains(entry.getKey())))
103104
.flatMap(entry -> entry.getValue().stream())
104105
.collect(Collectors.toSet());
105106

106-
ModuleFinder finder = ModuleSupport.ofSyntheticPluginModule(moduleName, jarPaths, requires, uses);
107-
ModuleLayer mparent = ModuleLayer.boot();
107+
ModuleFinder finder = ModuleSupport.ofSyntheticPluginModule(
108+
moduleName,
109+
jarPaths,
110+
requires,
111+
uses,
112+
s -> isPackageInLayers(s, parentLayer)
113+
);
108114
// TODO: check that denied modules are not brought as transitive dependencies (or switch to allow-list?)
109-
Configuration cf = mparent.configuration().resolve(finder, ModuleFinder.of(), Set.of(moduleName));
115+
Configuration cf = parentLayer.configuration().resolve(finder, ModuleFinder.of(), Set.of(moduleName));
110116

111117
Set<String> packageNames = finder.find(moduleName).map(ModuleReference::descriptor).map(ModuleDescriptor::packages).orElseThrow();
112118

@@ -115,12 +121,22 @@ static UberModuleClassLoader getInstance(ClassLoader parent, String moduleName,
115121
moduleName,
116122
jarUrls.toArray(new URL[0]),
117123
cf,
118-
mparent,
124+
parentLayer,
119125
packageNames
120126
);
121127
return AccessController.doPrivileged(pa);
122128
}
123129

130+
private static boolean isPackageInLayers(String packageName, ModuleLayer moduleLayer) {
131+
if (moduleLayer.modules().stream().map(Module::getPackages).anyMatch(p -> p.contains(packageName))) {
132+
return true;
133+
}
134+
if (moduleLayer.parents().equals(List.of(ModuleLayer.empty()))) {
135+
return false;
136+
}
137+
return moduleLayer.parents().stream().anyMatch(ml -> isPackageInLayers(packageName, ml));
138+
}
139+
124140
/**
125141
* Constructor
126142
*/

0 commit comments

Comments
 (0)