Skip to content

Commit 8042838

Browse files
committed
Apply suggestions from Claude code review
Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>
1 parent 5fa5c20 commit 8042838

File tree

2 files changed

+349
-281
lines changed

2 files changed

+349
-281
lines changed

app/src/main/java/org/hyperledger/besu/services/BesuPluginContextImpl.java

Lines changed: 19 additions & 281 deletions
Original file line numberDiff line numberDiff line change
@@ -18,45 +18,35 @@
1818
import static com.google.common.base.Preconditions.checkState;
1919

2020
import org.hyperledger.besu.ethereum.core.plugins.PluginConfiguration;
21-
import org.hyperledger.besu.ethereum.core.plugins.PluginsVerificationMode;
2221
import org.hyperledger.besu.plugin.BesuPlugin;
2322
import org.hyperledger.besu.plugin.ServiceManager;
2423
import org.hyperledger.besu.plugin.services.BesuService;
2524
import org.hyperledger.besu.plugin.services.PluginVersionsProvider;
26-
import org.hyperledger.besu.util.BesuVersionUtils;
2725

2826
import java.io.IOException;
2927
import java.net.MalformedURLException;
30-
import java.net.URI;
31-
import java.net.URISyntaxException;
3228
import java.net.URL;
3329
import java.net.URLClassLoader;
3430
import java.nio.file.Files;
3531
import java.nio.file.Path;
3632
import java.util.ArrayList;
3733
import java.util.Collections;
3834
import java.util.HashMap;
39-
import java.util.HashSet;
4035
import java.util.Iterator;
4136
import java.util.LinkedHashMap;
4237
import java.util.List;
4338
import java.util.Map;
4439
import java.util.NoSuchElementException;
45-
import java.util.Objects;
4640
import java.util.Optional;
4741
import java.util.ServiceLoader;
4842
import java.util.Set;
4943
import java.util.stream.Collectors;
5044
import java.util.stream.Stream;
5145
import java.util.stream.StreamSupport;
5246

53-
import com.fasterxml.jackson.core.type.TypeReference;
54-
import com.fasterxml.jackson.databind.ObjectMapper;
5547
import com.google.common.annotations.VisibleForTesting;
56-
import org.apache.commons.lang3.StringUtils;
5748
import org.slf4j.Logger;
5849
import org.slf4j.LoggerFactory;
59-
import org.slf4j.spi.LoggingEventBuilder;
6050

6151
/** The Besu plugin context implementation. */
6252
public class BesuPluginContextImpl implements ServiceManager, PluginVersionsProvider {
@@ -96,6 +86,7 @@ private enum Lifecycle {
9686

9787
private final Map<String, String> pluginVersions = new LinkedHashMap<>();
9888
private PluginConfiguration config;
89+
private URLClassLoader pluginClassLoader;
9990

10091
/** Instantiates a new Besu plugin context. */
10192
public BesuPluginContextImpl() {}
@@ -345,6 +336,16 @@ public void stopPlugins() {
345336
}
346337
}
347338

339+
// Close the plugin classloader to release file handles
340+
if (pluginClassLoader != null) {
341+
try {
342+
pluginClassLoader.close();
343+
LOG.debug("Closed plugin classloader");
344+
} catch (final IOException e) {
345+
LOG.debug("Error closing plugin classloader", e);
346+
}
347+
}
348+
348349
LOG.debug("Plugin shutdown complete.");
349350
state = Lifecycle.STOPPED;
350351
}
@@ -368,11 +369,16 @@ private List<BesuPlugin> detectPlugins(final PluginConfiguration config) {
368369
.filter(p -> p.getFileName().toString().endsWith(".jar"))
369370
.map(BesuPluginContextImpl::pathToURIOrNull)
370371
.toArray(URL[]::new);
371-
final var pluginClassLoader = new URLClassLoader(pluginJarURLs);
372+
// The URLClassLoader must remain open for the entire application lifecycle
373+
// as plugins may load classes lazily during their operation. The classloader
374+
// will be closed in stopPlugins() during shutdown.
375+
this.pluginClassLoader =
376+
new URLClassLoader(pluginJarURLs, this.getClass().getClassLoader());
372377
ServiceLoader<BesuPlugin> serviceLoader =
373-
ServiceLoader.load(BesuPlugin.class, pluginClassLoader);
378+
ServiceLoader.load(BesuPlugin.class, this.pluginClassLoader);
374379
final var foundPlugins = StreamSupport.stream(serviceLoader.spliterator(), false).toList();
375-
verifyPlugins(config.getPluginsVerificationMode(), pluginClassLoader, foundPlugins);
380+
PluginVerifier.verify(
381+
config.getPluginsVerificationMode(), this.pluginClassLoader, foundPlugins);
376382

377383
return foundPlugins;
378384
} catch (final MalformedURLException e) {
@@ -387,222 +393,6 @@ private List<BesuPlugin> detectPlugins(final PluginConfiguration config) {
387393
return List.of();
388394
}
389395

390-
private void verifyPlugins(
391-
final PluginsVerificationMode pluginsVerificationMode,
392-
final URLClassLoader pluginClassLoader,
393-
final List<BesuPlugin> plugins)
394-
throws IOException {
395-
396-
verifyNamesAreUnique(plugins);
397-
398-
final var pluginsArtifactData = getPluginsArtifactData(pluginClassLoader, plugins);
399-
LOG.debug("Loaded pluginsArtifactData: {}", pluginsArtifactData);
400-
401-
final var catalogLessArtifacts =
402-
pluginsArtifactData.stream()
403-
.filter(ad -> ad.catalog().equals(ArtifactCatalog.NO_CATALOG))
404-
.toList();
405-
406-
if (!catalogLessArtifacts.isEmpty()) {
407-
final LoggingEventBuilder leb =
408-
pluginsVerificationMode.failOnCatalogLess() ? LOG.atError() : LOG.atWarn();
409-
catalogLessArtifacts.forEach(
410-
ad ->
411-
leb.log(
412-
"Artifact {} containing plugins {} is without a catalog",
413-
ad.name(),
414-
ad.plugins().stream().map(BesuPlugin::getName).toList()));
415-
416-
if (pluginsVerificationMode.failOnCatalogLess()) {
417-
throw new IllegalStateException(
418-
"Plugins verification failed, see previous logs for more details.");
419-
}
420-
}
421-
422-
verifyCatalogs(pluginClassLoader, pluginsVerificationMode, pluginsArtifactData);
423-
}
424-
425-
private void verifyNamesAreUnique(final List<BesuPlugin> loadedPlugins) {
426-
final var pluginNameCounts =
427-
loadedPlugins.stream()
428-
.collect(Collectors.groupingBy(BesuPlugin::getName, Collectors.counting()));
429-
430-
final var duplicateNames =
431-
pluginNameCounts.entrySet().stream()
432-
.filter(entry -> entry.getValue() > 1)
433-
.map(Map.Entry::getKey)
434-
.toList();
435-
436-
if (!duplicateNames.isEmpty()) {
437-
throw new IllegalStateException(
438-
"Plugins with same name detected: " + String.join(", ", duplicateNames));
439-
}
440-
}
441-
442-
private List<ArtifactInfo> getPluginsArtifactData(
443-
final URLClassLoader pluginClassLoader, final List<BesuPlugin> plugins) throws IOException {
444-
final var pluginsByArtifact =
445-
plugins.stream()
446-
.collect(
447-
Collectors.groupingBy(
448-
plugin -> {
449-
try {
450-
return plugin
451-
.getClass()
452-
.getProtectionDomain()
453-
.getCodeSource()
454-
.getLocation()
455-
.toURI()
456-
.getSchemeSpecificPart();
457-
} catch (URISyntaxException e) {
458-
throw new RuntimeException(
459-
"Error getting artifact URL for plugin " + plugin.getName(), e);
460-
}
461-
}));
462-
463-
final var allCatalogs =
464-
Collections.list(pluginClassLoader.getResources("META-INF/plugin-artifacts-catalog.json"));
465-
466-
final var artifactsWithCatalogs =
467-
allCatalogs.stream()
468-
.collect(
469-
Collectors.toMap(
470-
url -> {
471-
try {
472-
return StringUtils.substringBefore(new URI(url.getPath()).getPath(), '!');
473-
} catch (URISyntaxException e) {
474-
throw new RuntimeException(e);
475-
}
476-
},
477-
ArtifactCatalog::load));
478-
479-
return pluginsByArtifact.entrySet().stream()
480-
.map(
481-
entry ->
482-
new ArtifactInfo(
483-
entry.getKey(),
484-
entry.getValue(),
485-
artifactsWithCatalogs.getOrDefault(entry.getKey(), ArtifactCatalog.NO_CATALOG)))
486-
.toList();
487-
}
488-
489-
private void verifyCatalogs(
490-
final URLClassLoader pluginClassLoader,
491-
final PluginsVerificationMode pluginsVerificationMode,
492-
final List<ArtifactInfo> pluginsArtifactData)
493-
throws IOException {
494-
verifyBesuVersions(pluginsVerificationMode, pluginsArtifactData);
495-
verifyBesuPluginsDependencyConflicts(
496-
pluginClassLoader, pluginsVerificationMode, pluginsArtifactData);
497-
verifyPluginsDependencyConflicts(pluginsVerificationMode, pluginsArtifactData);
498-
}
499-
500-
private void verifyBesuVersions(
501-
final PluginsVerificationMode pluginsVerificationMode,
502-
final List<ArtifactInfo> pluginsArtifactData) {
503-
final var besuRunningVersion = BesuVersionUtils.shortVersion();
504-
final var besuVersionConflictFound =
505-
pluginsArtifactData.stream()
506-
.anyMatch(
507-
artifactInfo -> {
508-
final var pluginBuildBesuVersion = artifactInfo.catalog().besuVersion();
509-
if (!besuRunningVersion.equals(pluginBuildBesuVersion)) {
510-
final LoggingEventBuilder leb =
511-
pluginsVerificationMode.failOnBesuVersionConflict()
512-
? LOG.atError()
513-
: LOG.atWarn();
514-
leb.log(
515-
"Plugin artifact {} is built against Besu version {} while current running Besu version is {}",
516-
artifactInfo.name(),
517-
pluginBuildBesuVersion == null ? "unknown" : pluginBuildBesuVersion,
518-
besuRunningVersion);
519-
return true;
520-
}
521-
return false;
522-
});
523-
524-
if (besuVersionConflictFound && pluginsVerificationMode.failOnBesuVersionConflict()) {
525-
throw new IllegalStateException(
526-
"Plugins verification failed, see previous logs for more details.");
527-
}
528-
}
529-
530-
private void verifyPluginsDependencyConflicts(
531-
final PluginsVerificationMode pluginsVerificationMode,
532-
final List<ArtifactInfo> pluginsArtifactInfo) {
533-
final var alreadyChecked = HashSet.<ArtifactInfo>newHashSet(pluginsArtifactInfo.size());
534-
535-
boolean verificationFailed = false;
536-
537-
for (final var artifactInfo : pluginsArtifactInfo) {
538-
alreadyChecked.add(artifactInfo);
539-
for (final var dep : artifactInfo.catalog().dependencies()) {
540-
for (final var otherArtifactInfo : pluginsArtifactInfo) {
541-
if (!alreadyChecked.contains(otherArtifactInfo)) {
542-
final var otherCatalog = otherArtifactInfo.catalog();
543-
final var maybeOtherDep = otherCatalog.getByNameAndGroup(dep);
544-
if (maybeOtherDep.isPresent()) {
545-
final var otherDep = maybeOtherDep.get();
546-
if (!dep.version().equals(otherDep.version())) {
547-
verificationFailed = pluginsVerificationMode.failOnPluginDependencyConflict();
548-
final LoggingEventBuilder leb = verificationFailed ? LOG.atError() : LOG.atWarn();
549-
leb.log(
550-
"Plugin artifacts {} and {} bring the same dependency {}:{} but with different versions: {} and {}",
551-
artifactInfo.name(),
552-
otherArtifactInfo.name(),
553-
dep.group(),
554-
dep.name(),
555-
dep.version(),
556-
otherDep.version());
557-
}
558-
}
559-
}
560-
}
561-
}
562-
}
563-
564-
if (verificationFailed) {
565-
throw new IllegalStateException(
566-
"Plugins verification failed, see previous logs for more details.");
567-
}
568-
}
569-
570-
private void verifyBesuPluginsDependencyConflicts(
571-
final URLClassLoader pluginClassLoader,
572-
final PluginsVerificationMode pluginsVerificationMode,
573-
final List<ArtifactInfo> pluginsArtifactData)
574-
throws IOException {
575-
final var besuArtifactsCatalog =
576-
Collections.list(pluginClassLoader.getResources("META-INF/besu-artifacts-catalog.json"));
577-
578-
final var besuDependencies = ArtifactDependency.loadList(besuArtifactsCatalog.get(0));
579-
580-
besuDependencies.forEach(
581-
besuDependency -> {
582-
final var conflicts =
583-
pluginsArtifactData.stream()
584-
.filter(pad -> pad.catalog().containsByNameAndGroup(besuDependency))
585-
.toList();
586-
if (!conflicts.isEmpty()) {
587-
final LoggingEventBuilder leb =
588-
pluginsVerificationMode.failOnBesuDependencyConflict()
589-
? LOG.atError()
590-
: LOG.atWarn();
591-
conflicts.forEach(
592-
ai -> {
593-
leb.log(
594-
"Plugin artifact {} brings the dependency {} that is already provided by Besu",
595-
ai.name(),
596-
besuDependency.group() + ":" + besuDependency.name());
597-
});
598-
if (pluginsVerificationMode.failOnBesuDependencyConflict()) {
599-
throw new IllegalStateException(
600-
"Plugins verification failed, see previous logs for more details.");
601-
}
602-
}
603-
});
604-
}
605-
606396
@Override
607397
public Map<String, String> getPluginVersions() {
608398
return Collections.unmodifiableMap(pluginVersions);
@@ -669,56 +459,4 @@ public List<String> getPluginsSummaryLog() {
669459

670460
return summary;
671461
}
672-
673-
record ArtifactInfo(String name, List<BesuPlugin> plugins, ArtifactCatalog catalog) {
674-
@Override
675-
public String toString() {
676-
return "ArtifactInfo{"
677-
+ "name='"
678-
+ name
679-
+ '\''
680-
+ ", plugins="
681-
+ plugins.stream().map(BesuPlugin::getName).collect(Collectors.joining(", "))
682-
+ ", catalog="
683-
+ catalog
684-
+ '}';
685-
}
686-
}
687-
688-
record ArtifactCatalog(String besuVersion, List<ArtifactDependency> dependencies) {
689-
static final ArtifactCatalog NO_CATALOG = new ArtifactCatalog(null, List.of());
690-
691-
static ArtifactCatalog load(final URL url) {
692-
final var objectMapper = new ObjectMapper();
693-
try {
694-
return objectMapper.readValue(url, ArtifactCatalog.class);
695-
} catch (IOException e) {
696-
throw new RuntimeException(e);
697-
}
698-
}
699-
700-
public boolean containsByNameAndGroup(final ArtifactDependency other) {
701-
return dependencies.stream().anyMatch(other::equalsByNameAndGroup);
702-
}
703-
704-
public Optional<ArtifactDependency> getByNameAndGroup(final ArtifactDependency other) {
705-
return dependencies.stream().filter(other::equalsByNameAndGroup).findFirst();
706-
}
707-
}
708-
709-
record ArtifactDependency(
710-
String name, String group, String version, String classifier, String filename) {
711-
boolean equalsByNameAndGroup(final ArtifactDependency other) {
712-
return Objects.equals(name, other.name) && Objects.equals(group, other.group);
713-
}
714-
715-
static List<ArtifactDependency> loadList(final URL url) {
716-
final var objectMapper = new ObjectMapper();
717-
try {
718-
return objectMapper.readValue(url, new TypeReference<>() {});
719-
} catch (IOException e) {
720-
throw new RuntimeException(e);
721-
}
722-
}
723-
}
724462
}

0 commit comments

Comments
 (0)