Skip to content

Commit

Permalink
Add strategy for handling the recovery of a plugin that could not be …
Browse files Browse the repository at this point in the history
…resolved (#564)
  • Loading branch information
decebals authored Feb 19, 2024
1 parent f086299 commit 336a5ba
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 81 deletions.
197 changes: 132 additions & 65 deletions pf4j/src/main/java/org/pf4j/AbstractPluginManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -109,6 +110,7 @@ public abstract class AbstractPluginManager implements PluginManager {
protected boolean exactVersionAllowed = false;

protected VersionManager versionManager;
protected ResolveRecoveryStrategy resolveRecoveryStrategy;

/**
* The plugins roots are supplied as comma-separated list by {@code System.getProperty("pf4j.pluginsDir", "plugins")}.
Expand Down Expand Up @@ -280,56 +282,55 @@ public boolean unloadPlugin(String pluginId) {
* @return true if the plugin was unloaded, otherwise false
*/
protected boolean unloadPlugin(String pluginId, boolean unloadDependents) {
try {
if (unloadDependents) {
List<String> dependents = dependencyResolver.getDependents(pluginId);
while (!dependents.isEmpty()) {
String dependent = dependents.remove(0);
unloadPlugin(dependent, false);
dependents.addAll(0, dependencyResolver.getDependents(dependent));
}
if (unloadDependents) {
List<String> dependents = dependencyResolver.getDependents(pluginId);
while (!dependents.isEmpty()) {
String dependent = dependents.remove(0);
unloadPlugin(dependent, false);
dependents.addAll(0, dependencyResolver.getDependents(dependent));
}
PluginWrapper pluginWrapper = getPlugin(pluginId);
PluginState pluginState;
try {
pluginState = stopPlugin(pluginId, false);
if (PluginState.STARTED == pluginState) {
return false;
}
}

log.info("Unload plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
} catch (Exception e) {
if (pluginWrapper == null) {
return false;
}
pluginState = PluginState.FAILED;
if (!plugins.containsKey(pluginId)) {
// nothing to do
return false;
}

PluginWrapper pluginWrapper = getPlugin(pluginId);
PluginState pluginState;
try {
pluginState = stopPlugin(pluginId, false);
if (PluginState.STARTED == pluginState) {
return false;
}

// remove the plugin
plugins.remove(pluginId);
getResolvedPlugins().remove(pluginWrapper);

firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));

// remove the classloader
Map<String, ClassLoader> pluginClassLoaders = getPluginClassLoaders();
if (pluginClassLoaders.containsKey(pluginId)) {
ClassLoader classLoader = pluginClassLoaders.remove(pluginId);
if (classLoader instanceof Closeable) {
try {
((Closeable) classLoader).close();
} catch (IOException e) {
throw new PluginRuntimeException(e, "Cannot close classloader");
}
log.info("Unload plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
} catch (Exception e) {
log.error("Cannot stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()), e);
pluginState = PluginState.FAILED;
}

// remove the plugin
plugins.remove(pluginId);
getResolvedPlugins().remove(pluginWrapper);
getUnresolvedPlugins().remove(pluginWrapper);

firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));

// remove the classloader
Map<String, ClassLoader> pluginClassLoaders = getPluginClassLoaders();
if (pluginClassLoaders.containsKey(pluginId)) {
ClassLoader classLoader = pluginClassLoaders.remove(pluginId);
if (classLoader instanceof Closeable) {
try {
((Closeable) classLoader).close();
} catch (IOException e) {
throw new PluginRuntimeException(e, "Cannot close classloader");
}
}

return true;
} catch (IllegalArgumentException e) {
// ignore not found exceptions because this method is recursive
}

return false;
return true;
}

@Override
Expand Down Expand Up @@ -506,11 +507,11 @@ protected PluginState stopPlugin(String pluginId, boolean stopDependents) {
* Check if the plugin exists in the list of plugins.
*
* @param pluginId the pluginId to check
* @throws IllegalArgumentException if the plugin does not exist
* @throws PluginNotFoundException if the plugin does not exist
*/
protected void checkPluginId(String pluginId) {
if (!plugins.containsKey(pluginId)) {
throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
throw new PluginNotFoundException(pluginId);
}
}

Expand Down Expand Up @@ -749,6 +750,7 @@ protected void initialize() {

versionManager = createVersionManager();
dependencyResolver = new DependencyResolver(versionManager);
resolveRecoveryStrategy = ResolveRecoveryStrategy.THROW_EXCEPTION;
}

/**
Expand Down Expand Up @@ -814,27 +816,7 @@ protected boolean isPluginDisabled(String pluginId) {
* @throws PluginRuntimeException if something goes wrong
*/
protected void resolvePlugins() {
// retrieves the plugins descriptors
List<PluginDescriptor> descriptors = plugins.values().stream()
.map(PluginWrapper::getDescriptor)
.collect(Collectors.toList());

DependencyResolver.Result result = dependencyResolver.resolve(descriptors);

if (result.hasCyclicDependency()) {
throw new DependencyResolver.CyclicDependencyException();
}

List<String> notFoundDependencies = result.getNotFoundDependencies();
if (!notFoundDependencies.isEmpty()) {
throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies);
}

List<DependencyResolver.WrongDependencyVersion> wrongVersionDependencies = result.getWrongVersionDependencies();
if (!wrongVersionDependencies.isEmpty()) {
throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies);
}

DependencyResolver.Result result = resolveDependencies();
List<String> sortedPlugins = result.getSortedPlugins();

// move plugins from "unresolved" to "resolved"
Expand Down Expand Up @@ -1063,4 +1045,89 @@ protected <T> List<T> getExtensions(List<ExtensionWrapper<T>> extensionsWrapper)
return extensions;
}

protected DependencyResolver.Result resolveDependencies() {
// retrieves the plugins descriptors
List<PluginDescriptor> descriptors = plugins.values().stream()
.map(PluginWrapper::getDescriptor)
.collect(Collectors.toList());

DependencyResolver.Result result = dependencyResolver.resolve(descriptors);

if (result.isOK()) {
return result;
}

if (result.hasCyclicDependency()) {
// cannot recover from cyclic dependency
throw new DependencyResolver.CyclicDependencyException();
}

List<String> notFoundDependencies = result.getNotFoundDependencies();
if (result.hasNotFoundDependencies() && resolveRecoveryStrategy.equals(ResolveRecoveryStrategy.THROW_EXCEPTION)) {
throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies);
}

List<DependencyResolver.WrongDependencyVersion> wrongVersionDependencies = result.getWrongVersionDependencies();
if (result.hasWrongVersionDependencies() && resolveRecoveryStrategy.equals(ResolveRecoveryStrategy.THROW_EXCEPTION)) {
throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies);
}

List<PluginDescriptor> resolvedDescriptors = new ArrayList<>(descriptors);

for (String notFoundDependency : notFoundDependencies) {
List<String> dependents = dependencyResolver.getDependents(notFoundDependency);
dependents.forEach(dependent -> resolvedDescriptors.removeIf(descriptor -> descriptor.getPluginId().equals(dependent)));
}

for (DependencyResolver.WrongDependencyVersion wrongVersionDependency : wrongVersionDependencies) {
resolvedDescriptors.removeIf(descriptor -> descriptor.getPluginId().equals(wrongVersionDependency.getDependencyId()));
}

List<PluginDescriptor> unresolvedDescriptors = new ArrayList<>(descriptors);
unresolvedDescriptors.removeAll(resolvedDescriptors);

for (PluginDescriptor unresolvedDescriptor : unresolvedDescriptors) {
unloadPlugin(unresolvedDescriptor.getPluginId(), false);
}

return resolveDependencies();
}

/**
* Retrieve the strategy for handling the recovery of a plugin resolve (load) failure.
* Default is {@link ResolveRecoveryStrategy#THROW_EXCEPTION}.
*
* @return the strategy
*/
protected final ResolveRecoveryStrategy getResolveRecoveryStrategy() {
return resolveRecoveryStrategy;
}

/**
* Set the strategy for handling the recovery of a plugin resolve (load) failure.
*
* @param resolveRecoveryStrategy the strategy
*/
protected void setResolveRecoveryStrategy(ResolveRecoveryStrategy resolveRecoveryStrategy) {
Objects.requireNonNull(resolveRecoveryStrategy, "resolveRecoveryStrategy cannot be null");
this.resolveRecoveryStrategy = resolveRecoveryStrategy;
}

/**
* Strategy for handling the recovery of a plugin that could not be resolved
* (loaded) due to a dependency problem.
*/
public enum ResolveRecoveryStrategy {

/**
* Throw an exception when a resolve (load) failure occurs.
*/
THROW_EXCEPTION,
/**
* Ignore the plugin with the resolve (load) failure and continue.
* The plugin with problems will be removed/unloaded from the plugins list.
*/
IGNORE_PLUGIN_AND_CONTINUE
}

}
29 changes: 28 additions & 1 deletion pf4j/src/main/java/org/pf4j/DependencyResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ public boolean hasCyclicDependency() {
return cyclicDependency;
}

/**
* Returns true if there are dependencies required that were not found.
*
* @return true if there are dependencies required that were not found
*/
public boolean hasNotFoundDependencies() {
return !notFoundDependencies.isEmpty();
}

/**
* Returns a list with dependencies required that were not found.
*
Expand All @@ -226,6 +235,15 @@ public List<String> getNotFoundDependencies() {
return notFoundDependencies;
}

/**
* Returns true if there are dependencies with wrong version.
*
* @return true if there are dependencies with wrong version
*/
public boolean hasWrongVersionDependencies() {
return !wrongVersionDependencies.isEmpty();
}

/**
* Returns a list with dependencies with wrong version.
*
Expand All @@ -235,6 +253,15 @@ public List<WrongDependencyVersion> getWrongVersionDependencies() {
return wrongVersionDependencies;
}

/**
* Returns true if the result is OK (no cyclic dependency, no not found dependencies, no wrong version dependencies).
*
* @return true if the result is OK
*/
public boolean isOK() {
return !hasCyclicDependency() && !hasNotFoundDependencies() && !hasWrongVersionDependencies();
}

/**
* Get the list of plugins in dependency sorted order.
*
Expand Down Expand Up @@ -333,7 +360,7 @@ public List<String> getDependencies() {
*/
public static class DependenciesWrongVersionException extends PluginRuntimeException {

private List<WrongDependencyVersion> dependencies;
private final List<WrongDependencyVersion> dependencies;

public DependenciesWrongVersionException(List<WrongDependencyVersion> dependencies) {
super("Dependencies '{}' have wrong version", dependencies);
Expand Down
37 changes: 37 additions & 0 deletions pf4j/src/main/java/org/pf4j/PluginNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pf4j;

/**
* Thrown when a plugin is not found.
*
* @author Decebal Suiu
*/
public class PluginNotFoundException extends PluginRuntimeException {

private final String pluginId;

public PluginNotFoundException(String pluginId) {
super("Plugin '{}' not found", pluginId);

this.pluginId = pluginId;
}

public String getPluginId() {
return pluginId;
}

}
6 changes: 2 additions & 4 deletions pf4j/src/test/java/org/pf4j/AbstractPluginManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.pf4j.test.TestExtension;
import org.pf4j.test.TestExtensionPoint;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -118,7 +116,7 @@ void checkExistedPluginId() {

@Test
void checkNotExistedPluginId() {
assertThrows(IllegalArgumentException.class, () -> pluginManager.checkPluginId("plugin1"));
assertThrows(PluginNotFoundException.class, () -> pluginManager.checkPluginId("plugin1"));
}

private PluginWrapper createPluginWrapper(String pluginId) {
Expand All @@ -127,7 +125,7 @@ private PluginWrapper createPluginWrapper(String pluginId) {
pluginDescriptor.setPluginVersion("1.2.3");

PluginWrapper pluginWrapper = new PluginWrapper(pluginManager, pluginDescriptor, Paths.get("plugin1"), getClass().getClassLoader());
Plugin plugin= mock(Plugin.class);
Plugin plugin = mock(Plugin.class);
pluginWrapper.setPluginFactory(wrapper -> plugin);

return pluginWrapper;
Expand Down
Loading

0 comments on commit 336a5ba

Please sign in to comment.