Browse Source

Add strategy for handling the recovery of a plugin that could not be resolved (#564)

tags/release-3.11.0
Decebal Suiu 2 months ago
parent
commit
336a5ba35a
No account linked to committer's email address

+ 132
- 65
pf4j/src/main/java/org/pf4j/AbstractPluginManager.java View File

import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;


protected boolean exactVersionAllowed = false; protected boolean exactVersionAllowed = false;


protected VersionManager versionManager; protected VersionManager versionManager;
protected ResolveRecoveryStrategy resolveRecoveryStrategy;


/** /**
* The plugins roots are supplied as comma-separated list by {@code System.getProperty("pf4j.pluginsDir", "plugins")}. * The plugins roots are supplied as comma-separated list by {@code System.getProperty("pf4j.pluginsDir", "plugins")}.
* @return true if the plugin was unloaded, otherwise false * @return true if the plugin was unloaded, otherwise false
*/ */
protected boolean unloadPlugin(String pluginId, boolean unloadDependents) { 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 @Override
* Check if the plugin exists in the list of plugins. * Check if the plugin exists in the list of plugins.
* *
* @param pluginId the pluginId to check * @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) { protected void checkPluginId(String pluginId) {
if (!plugins.containsKey(pluginId)) { if (!plugins.containsKey(pluginId)) {
throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
throw new PluginNotFoundException(pluginId);
} }
} }




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


/** /**
* @throws PluginRuntimeException if something goes wrong * @throws PluginRuntimeException if something goes wrong
*/ */
protected void resolvePlugins() { 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(); List<String> sortedPlugins = result.getSortedPlugins();


// move plugins from "unresolved" to "resolved" // move plugins from "unresolved" to "resolved"
return extensions; 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
}

} }

+ 28
- 1
pf4j/src/main/java/org/pf4j/DependencyResolver.java View File

return cyclicDependency; 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. * Returns a list with dependencies required that were not found.
* *
return notFoundDependencies; 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. * Returns a list with dependencies with wrong version.
* *
return wrongVersionDependencies; 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. * Get the list of plugins in dependency sorted order.
* *
*/ */
public static class DependenciesWrongVersionException extends PluginRuntimeException { public static class DependenciesWrongVersionException extends PluginRuntimeException {


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


public DependenciesWrongVersionException(List<WrongDependencyVersion> dependencies) { public DependenciesWrongVersionException(List<WrongDependencyVersion> dependencies) {
super("Dependencies '{}' have wrong version", dependencies); super("Dependencies '{}' have wrong version", dependencies);

+ 37
- 0
pf4j/src/main/java/org/pf4j/PluginNotFoundException.java View File

/*
* 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;
}

}

+ 2
- 4
pf4j/src/test/java/org/pf4j/AbstractPluginManagerTest.java View File

import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.pf4j.test.TestExtension; import org.pf4j.test.TestExtension;
import org.pf4j.test.TestExtensionPoint; import org.pf4j.test.TestExtensionPoint;


import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;


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


private PluginWrapper createPluginWrapper(String pluginId) { private PluginWrapper createPluginWrapper(String pluginId) {
pluginDescriptor.setPluginVersion("1.2.3"); pluginDescriptor.setPluginVersion("1.2.3");


PluginWrapper pluginWrapper = new PluginWrapper(pluginManager, pluginDescriptor, Paths.get("plugin1"), getClass().getClassLoader()); 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); pluginWrapper.setPluginFactory(wrapper -> plugin);


return pluginWrapper; return pluginWrapper;

+ 107
- 11
pf4j/src/test/java/org/pf4j/DefaultPluginManagerTest.java View File



import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;


@Test @Test
public void loadedPluginWithMissingDependencyCanBeUnloaded() throws IOException { public void loadedPluginWithMissingDependencyCanBeUnloaded() throws IOException {
pluginManager.setResolveRecoveryStrategy(AbstractPluginManager.ResolveRecoveryStrategy.IGNORE_PLUGIN_AND_CONTINUE);

PluginZip pluginZip = new PluginZip.Builder(pluginsPath.resolve("my-plugin-1.1.1.zip"), "myPlugin") PluginZip pluginZip = new PluginZip.Builder(pluginsPath.resolve("my-plugin-1.1.1.zip"), "myPlugin")
.pluginVersion("1.1.1") .pluginVersion("1.1.1")
.pluginDependencies("myBasePlugin") .pluginDependencies("myBasePlugin")
.build(); .build();


try {
pluginManager.loadPlugin(pluginZip.path());
} catch (DependencyResolver.DependenciesNotFoundException e) {
// expected
}
// try to load the plugin with a missing dependency
pluginManager.loadPlugin(pluginZip.path());

// the plugin is unloaded automatically
assertTrue(pluginManager.getPlugins().isEmpty());

// start plugins
pluginManager.startPlugins();

assertTrue(pluginManager.getStartedPlugins().isEmpty());
assertTrue(pluginManager.getUnresolvedPlugins().isEmpty());
assertTrue(pluginManager.getResolvedPlugins().isEmpty());
assertTrue(pluginManager.getPlugins().isEmpty());
}

@Test
void loadingPluginWithMissingDependencyDoesNotBreakOtherPlugins() throws IOException {
pluginManager.setResolveRecoveryStrategy(AbstractPluginManager.ResolveRecoveryStrategy.IGNORE_PLUGIN_AND_CONTINUE);

// Load 2 plugins, one with a dependency that is missing and one without any dependencies.
PluginZip pluginZip1 = new PluginZip.Builder(pluginsPath.resolve("my-first-plugin-1.1.1.zip"), "myPlugin1")
.pluginVersion("1.1.1")
.pluginDependencies("myBasePlugin")
.build();

PluginZip pluginZip2 = new PluginZip.Builder(pluginsPath.resolve("my-second-plugin-2.2.2.zip"), "myPlugin2")
.pluginVersion("2.2.2")
.build();


pluginManager.loadPlugins();
pluginManager.startPlugins(); pluginManager.startPlugins();


PluginWrapper myPlugin = pluginManager.getPlugin(pluginZip.pluginId());
assertNotNull(myPlugin);
assertNotEquals(PluginState.STARTED, myPlugin.getPluginState());
// myPlugin2 should have been started at this point.
assertEquals(PluginState.STARTED, pluginManager.getPlugin(pluginZip2.pluginId()).getPluginState());

pluginManager.unloadPlugin(pluginZip1.pluginId());

// No traces should remain of myPlugin1.
assertTrue(
pluginManager.getUnresolvedPlugins().stream()
.noneMatch(pluginWrapper -> pluginWrapper.getPluginId().equals(pluginZip1.pluginId()))
);

pluginManager.unloadPlugin(pluginZip2.pluginId());

// Load the missing dependency, everything should start working.
PluginZip pluginZipBase = new PluginZip.Builder(pluginsPath.resolve("my-base-plugin-3.0.0.zip"), "myBasePlugin")
.pluginVersion("3.0.0")
.build();

pluginManager.loadPlugins();
pluginManager.startPlugins();

assertEquals(PluginState.STARTED, pluginManager.getPlugin(pluginZip1.pluginId()).getPluginState());
assertEquals(PluginState.STARTED, pluginManager.getPlugin(pluginZip2.pluginId()).getPluginState());
assertEquals(PluginState.STARTED, pluginManager.getPlugin(pluginZipBase.pluginId()).getPluginState());

assertTrue(pluginManager.getUnresolvedPlugins().isEmpty());
}

@Test
void loadingPluginWithMissingDependencyFails() throws IOException {
pluginManager.setResolveRecoveryStrategy(AbstractPluginManager.ResolveRecoveryStrategy.THROW_EXCEPTION);

PluginZip pluginZip = new PluginZip.Builder(pluginsPath.resolve("my-plugin-1.1.1.zip"), "myPlugin")
.pluginVersion("1.1.1")
.pluginDependencies("myBasePlugin")
.build();

// try to load the plugin with a missing dependency
Path pluginPath = pluginZip.path();
assertThrows(DependencyResolver.DependenciesNotFoundException.class, () -> pluginManager.loadPlugin(pluginPath));
}

@Test
void loadingPluginWithWrongDependencyVersionFails() throws IOException {
pluginManager.setResolveRecoveryStrategy(AbstractPluginManager.ResolveRecoveryStrategy.THROW_EXCEPTION);

PluginZip pluginZip1 = new PluginZip.Builder(pluginsPath.resolve("my-first-plugin-1.1.1.zip"), "myPlugin1")
.pluginVersion("1.1.1")
.pluginDependencies("myPlugin2@3.0.0")
.build();

PluginZip pluginZip2 = new PluginZip.Builder(pluginsPath.resolve("my-second-plugin-2.2.2.zip"), "myPlugin2")
.pluginVersion("2.2.2")
.build();

// try to load the plugins with cyclic dependencies
Path pluginPath1 = pluginZip1.path();
Path pluginPath2 = pluginZip2.path();
assertThrows(DependencyResolver.DependenciesWrongVersionException.class, () -> pluginManager.loadPlugins());
}

@Test
void loadingPluginsWithCyclicDependenciesFails() throws IOException {
PluginZip pluginZip1 = new PluginZip.Builder(pluginsPath.resolve("my-first-plugin-1.1.1.zip"), "myPlugin1")
.pluginVersion("1.1.1")
.pluginDependencies("myPlugin2")
.build();

PluginZip pluginZip2 = new PluginZip.Builder(pluginsPath.resolve("my-second-plugin-2.2.2.zip"), "myPlugin2")
.pluginVersion("2.2.2")
.pluginDependencies("myPlugin1")
.build();


assertTrue(pluginManager.unloadPlugin(pluginZip.pluginId()));
// try to load the plugins with cyclic dependencies
Path pluginPath1 = pluginZip1.path();
Path pluginPath2 = pluginZip2.path();
assertThrows(DependencyResolver.CyclicDependencyException.class, () -> pluginManager.loadPlugins());
} }


} }

Loading…
Cancel
Save