NIFI-13795 Improve NAR Manager handling of dependent NARs (#9310)

- Ensure NARs are loaded in parent-first order during restart
- Allow replacing a NAR when other NARs are dependent on it
- Improvements to catch Throwable in several places to handle NoClassDefFoundError

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Bryan Bende 2024-09-26 13:28:17 -04:00 committed by GitHub
parent f39cce5dec
commit ee29562517
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 230 additions and 107 deletions

View File

@ -41,6 +41,11 @@ public class NarPersistenceInfo {
return narProperties;
}
@Override
public String toString() {
return "%s:%s:%s".formatted(narProperties.getNarGroup(), narProperties.getNarId(), narProperties.getNarVersion());
}
@Override
public boolean equals(final Object o) {
if (this == o) {

View File

@ -55,7 +55,7 @@ public interface NarManager {
* @param coordinate the coordinate of the NAR
* @param narState the new state
*/
void updateState(BundleCoordinate coordinate, NarState narState);
void updateState(BundleCoordinate coordinate, NarState narState, String failureMessage);
/**
* @return all NARs contained in the NAR Manager

View File

@ -440,10 +440,10 @@ public class ExtensionBuilder {
final StandardLoggingContext loggingContext = new StandardLoggingContext(null);
try {
return createControllerServiceNode(loggingContext);
} catch (final Exception e) {
logger.error("Could not create Controller Service of type {} from {} for ID {} due to: {}; creating \"Ghost\" implementation", type, bundleCoordinate, identifier, e.getMessage());
} catch (final Throwable t) {
logger.error("Could not create Controller Service of type {} from {} for ID {} due to: {}; creating \"Ghost\" implementation", type, bundleCoordinate, identifier, t.getMessage());
if (logger.isDebugEnabled()) {
logger.debug(e.getMessage(), e);
logger.debug(t.getMessage(), t);
}
return createGhostControllerServiceNode();
@ -790,8 +790,8 @@ public class ExtensionBuilder {
verifyControllerServiceReferences(processor, bundle.getClassLoader());
return processorComponent;
} catch (final Exception e) {
throw new ProcessorInstantiationException(type, e);
} catch (final Throwable t) {
throw new ProcessorInstantiationException(type, t);
}
}
@ -810,8 +810,8 @@ public class ExtensionBuilder {
verifyControllerServiceReferences(taskComponent.getComponent(), bundle.getClassLoader());
return taskComponent;
} catch (final Exception e) {
throw new ReportingTaskInstantiationException(type, e);
} catch (final Throwable t) {
throw new ReportingTaskInstantiationException(type, t);
}
}
@ -825,8 +825,8 @@ public class ExtensionBuilder {
loggableComponent.getComponent().initialize(config);
return loggableComponent;
} catch (final Exception e) {
throw new FlowAnalysisRuleInstantiationException(type, e);
} catch (final Throwable t) {
throw new FlowAnalysisRuleInstantiationException(type, t);
}
}
@ -860,8 +860,8 @@ public class ExtensionBuilder {
return clientComponent;
} catch (final Exception e) {
throw new FlowRepositoryClientInstantiationException(type, e);
} catch (final Throwable t) {
throw new FlowRepositoryClientInstantiationException(type, t);
}
}
@ -879,8 +879,8 @@ public class ExtensionBuilder {
verifyControllerServiceReferences(providerComponent.getComponent(), bundle.getClassLoader());
return providerComponent;
} catch (final Exception e) {
throw new ParameterProviderInstantiationException(type, e);
} catch (final Throwable t) {
throw new ParameterProviderInstantiationException(type, t);
}
}

View File

@ -31,8 +31,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@ -77,49 +82,69 @@ public class NarInstallTask implements Runnable {
try {
// If replacing an existing NAR with the same coordinate, then unload the existing NAR and stop+ghost any components from it
// If the NAR being replaced contains Python extensions, those need to be included through a separate lookup since their bundle coordinate is a logical python coordinate
final List<File> narsToLoad = new ArrayList<>();
final StandardStoppedComponents stoppedComponents = new StandardStoppedComponents(controllerServiceProvider);
final Bundle existingBundle = extensionManager.getBundle(coordinate);
if (existingBundle == null) {
LOGGER.info("Installing NAR [{}] with coordinate [{}]", narNode.getIdentifier(), coordinate);
final List<File> unloadedNarFiles = unloadNarChain(coordinate, stoppedComponents, false);
narsToLoad.addAll(unloadedNarFiles);
narsToLoad.add(narFile);
} else {
LOGGER.info("Replacing NAR [{}], unloading existing NAR and components", coordinate);
final Set<ExtensionDefinition> extensionDefinitions = new HashSet<>(extensionManager.getTypes(coordinate));
extensionDefinitions.addAll(extensionManager.getPythonExtensions(coordinate));
narLoader.unload(existingBundle);
narComponentManager.unloadComponents(coordinate, extensionDefinitions, stoppedComponents);
final List<File> unloadedNarFiles = unloadNarChain(coordinate, stoppedComponents, true);
narsToLoad.addAll(unloadedNarFiles);
}
// Attempt to load the NAR which will include any NARs that were previously skipped
final NarLoadResult narLoadResult = narLoader.load(Collections.singleton(narFile), ALLOWED_EXTENSION_TYPES);
// Attempt to load the NAR and any NARs that were unloaded, or need to be reloaded, and the NAR loader will also include any NARs that were previously skipped
// Unloading was done in child first order, so we need to reverse the list to load in parent first order
Collections.reverse(narsToLoad);
LOGGER.info("Install task for [{}] will load NARs {}", coordinate, narsToLoad);
final NarLoadResult narLoadResult = narLoader.load(narsToLoad, ALLOWED_EXTENSION_TYPES);
// For any successfully loaded NARs, un-ghost any components that can be provided by one of the loaded NARs, this handles a general ghosting case where
// the NAR now becomes available, as well as restoring any component that may have been purposely unloaded above for replacing an existing NAR
for (final Bundle loadedBundle : narLoadResult.getLoadedBundles()) {
boolean installed = false;
final BundleCoordinate loadedCoordinate = loadedBundle.getBundleDetails().getCoordinate();
LOGGER.info("NAR [{}] was installed", loadedCoordinate);
LOGGER.info("NAR [{}] was loaded", loadedCoordinate);
if (loadedCoordinate.equals(coordinate)) {
// If the NAR that was just uploaded was successfully loaded, attempt to access the class of each extension to prove that each
// class can load successfully, if not then we want to bounce out to the catch block and set the state as FAILED
final Set<ExtensionDefinition> loadedExtensionDefinitions = extensionManager.getTypes(coordinate);
for (final ExtensionDefinition loadedExtensionDefinition : loadedExtensionDefinitions) {
final Class<?> extensionClass = extensionManager.getClass(loadedExtensionDefinition);
LOGGER.debug("Loaded [{}] from bundle [{}]", extensionClass.getCanonicalName(), coordinate);
// class can load successfully, if not then we want to set the state as FAILED and capture the error message
try {
verifyExtensionDefinitions(coordinate);
narNode.setState(NarState.INSTALLED);
installed = true;
} catch (final Throwable t) {
LOGGER.error("Failed to install NAR [{}]", coordinate, t);
narNode.setState(NarState.FAILED);
narNode.setFailureMessage(t.getMessage());
}
narNode.setState(NarState.INSTALLED);
} else {
try {
narManager.updateState(loadedCoordinate, NarState.INSTALLED);
verifyExtensionDefinitions(loadedCoordinate);
narManager.updateState(loadedCoordinate, NarState.INSTALLED, null);
installed = true;
} catch (final NarNotFoundException e) {
LOGGER.warn("NAR [{}] was loaded, but no longer exists in the NAR Manager", loadedCoordinate);
} catch (final Throwable t) {
LOGGER.error("Failed to install NAR [{}]", coordinate, t);
narManager.updateState(loadedCoordinate, NarState.FAILED, t.getMessage());
}
}
final Set<ExtensionDefinition> extensionDefinitions = new HashSet<>(extensionManager.getTypes(loadedCoordinate));
extensionDefinitions.addAll(extensionManager.getPythonExtensions(loadedCoordinate));
narComponentManager.loadMissingComponents(loadedCoordinate, extensionDefinitions, stoppedComponents);
if (installed) {
try {
final Set<ExtensionDefinition> extensionDefinitions = new HashSet<>(extensionManager.getTypes(loadedCoordinate));
extensionDefinitions.addAll(extensionManager.getPythonExtensions(loadedCoordinate));
narComponentManager.loadMissingComponents(loadedCoordinate, extensionDefinitions, stoppedComponents);
} catch (final Throwable t) {
LOGGER.error("Failed to load missing components from [{}]", loadedCoordinate, t);
}
}
}
// Process the skipped bundles and mark them as having a missing dependency
for (final BundleDetails skippedBundles : narLoadResult.getSkippedBundles()) {
final BundleCoordinate skippedCoordinate = skippedBundles.getCoordinate();
LOGGER.info("NAR [{}] is missing dependency", skippedCoordinate);
@ -127,7 +152,7 @@ public class NarInstallTask implements Runnable {
narNode.setState(NarState.MISSING_DEPENDENCY);
} else {
try {
narManager.updateState(skippedCoordinate, NarState.MISSING_DEPENDENCY);
narManager.updateState(skippedCoordinate, NarState.MISSING_DEPENDENCY, null);
} catch (final NarNotFoundException e) {
LOGGER.warn("NAR [{}] was skipped, but no longer exists in the NAR Manager", skippedCoordinate);
}
@ -137,7 +162,7 @@ public class NarInstallTask implements Runnable {
// Restore previously running/enabled components to their original state
stoppedComponents.startAll();
// Notify the NAR Manager that the install task complete for the current NAR
// Notify the NAR Manager that the install task completed for the current NAR
narManager.completeInstall(narNode.getIdentifier());
} catch (final Throwable t) {
@ -147,6 +172,84 @@ public class NarInstallTask implements Runnable {
}
}
private void verifyExtensionDefinitions(final BundleCoordinate coordinate) {
final Set<ExtensionDefinition> loadedExtensionDefinitions = extensionManager.getTypes(coordinate);
for (final ExtensionDefinition loadedExtensionDefinition : loadedExtensionDefinitions) {
final Class<?> extensionClass = extensionManager.getClass(loadedExtensionDefinition);
LOGGER.debug("Loaded [{}] from bundle [{}]", extensionClass.getCanonicalName(), coordinate);
}
}
private List<File> unloadNarChain(final BundleCoordinate coordinate, final StoppedComponents stoppedComponents, final boolean includeStartingNar) {
final Map<BundleCoordinate, File> narFilesByCoordinate = new HashMap<>();
narManager.getNars().forEach(narNode -> narFilesByCoordinate.put(narNode.getManifest().getCoordinate(), narNode.getNarFile()));
final List<File> unloadedNarFiles = new ArrayList<>();
final List<Bundle> bundlesToUnload = new ArrayList<>();
final Map<BundleCoordinate, Set<ExtensionDefinition>> extensionsByCoordinate = new LinkedHashMap<>();
final List<Bundle> bundleChain = getDependencyChain(coordinate, includeStartingNar);
for (final Bundle chainedBundle : bundleChain) {
final BundleCoordinate chainedCoordinate = chainedBundle.getBundleDetails().getCoordinate();
final File chainedNarFile = narFilesByCoordinate.get(chainedCoordinate);
if (chainedNarFile != null) {
final Set<ExtensionDefinition> extensionDefinitions = new HashSet<>(extensionManager.getTypes(chainedCoordinate));
extensionDefinitions.addAll(extensionManager.getPythonExtensions(chainedCoordinate));
extensionsByCoordinate.put(chainedCoordinate, extensionDefinitions);
bundlesToUnload.add(chainedBundle);
unloadedNarFiles.add(chainedNarFile);
} else {
LOGGER.warn("Found NAR [{}] in dependency chain of [{}], but it is not present in the NAR Manager", chainedCoordinate, coordinate);
}
}
LOGGER.debug("Unloading NAR chain from [{}] requires unloading {}", coordinate, bundlesToUnload);
narLoader.unload(bundlesToUnload);
for (Map.Entry<BundleCoordinate, Set<ExtensionDefinition>> entry : extensionsByCoordinate.entrySet()) {
narComponentManager.unloadComponents(entry.getKey(), entry.getValue(), stoppedComponents);
}
return unloadedNarFiles;
}
/**
* Gets the depth-first list of dependents from this bundle, including the starting bundle.
*
* If bundle A is dependent on B which is dependent on C, then...
* - getDependencyChain(A) returns [A]
* - getDependencyChain(B) returns [A, B]
* - getDependencyChain(C) returns [A, B, C]
*
* @param startingCoordinate the coordinate to start building the chain from
* @param includeStartingBundle indicates if the bundle for the starting coordinate should be included in the chain
* @return the depth-first list of dependents
*/
private List<Bundle> getDependencyChain(final BundleCoordinate startingCoordinate, final boolean includeStartingBundle) {
final List<Bundle> dependencyChain = new ArrayList<>();
traverseDependencyChain(startingCoordinate, dependencyChain);
if (includeStartingBundle) {
final Bundle startingBundle = extensionManager.getBundle(startingCoordinate);
if (startingBundle != null) {
dependencyChain.add(startingBundle);
}
}
return dependencyChain;
}
private void traverseDependencyChain(final BundleCoordinate currentCoordinate, final List<Bundle> dependencyChain) {
final Set<Bundle> dependentBundles = extensionManager.getDependentBundles(currentCoordinate);
if (dependentBundles.isEmpty()) {
return;
}
for (final Bundle dependentBundle : dependentBundles) {
final BundleCoordinate dependentCoordinate = dependentBundle.getBundleDetails().getCoordinate();
traverseDependencyChain(dependentCoordinate, dependencyChain);
dependencyChain.add(dependentBundle);
}
}
public static Builder builder() {
return new Builder();
}

View File

@ -45,17 +45,19 @@ import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@ -105,8 +107,8 @@ public class StandardNarManager implements NarManager, InitializingBean, Closeab
*/
@Override
public void afterPropertiesSet() throws IOException {
final Collection<NarPersistenceInfo> narInfos = loadExistingNars();
restoreState(narInfos);
final Collection<NarPersistenceInfo> narInfos = getExistingNarInfos();
restoreNarNodes(narInfos);
}
@Override
@ -139,12 +141,13 @@ public class StandardNarManager implements NarManager, InitializingBean, Closeab
}
@Override
public synchronized void updateState(final BundleCoordinate coordinate, final NarState narState) {
public synchronized void updateState(final BundleCoordinate coordinate, final NarState narState, final String failureMessage) {
final NarNode narNode = narNodesById.values().stream()
.filter(n -> n.getManifest().getCoordinate().equals(coordinate))
.findFirst()
.orElseThrow(() -> new NarNotFoundException(coordinate));
narNode.setState(narState);
narNode.setFailureMessage(failureMessage);
}
@Override
@ -344,49 +347,75 @@ public class StandardNarManager implements NarManager, InitializingBean, Closeab
}
}
private Collection<NarPersistenceInfo> loadExistingNars() throws IOException {
private Collection<NarPersistenceInfo> getExistingNarInfos() throws IOException {
final Collection<NarPersistenceInfo> narInfos = persistenceProvider.getAllNarInfo();
logger.info("Loading stored NAR files [{}]", narInfos.size());
if (narInfos.isEmpty()) {
return narInfos;
}
final Collection<File> narFiles = narInfos.stream()
.map(NarPersistenceInfo::getNarFile)
.collect(Collectors.toSet());
narLoader.load(narFiles);
return narInfos;
logger.debug("NAR Infos before reordering: {}", narInfos);
// If any NARs being loaded are parents of other NARs being loaded, we need to ensure parents load before children, otherwise a child NAR may
// select a compatible parent NAR from the other NARs provided by NiFi, rather than selecting the parent NAR from within the NAR Manager
final int numNarInfos = narInfos.size();
final Stack<NarPersistenceInfo> narHierarchy = new Stack<>();
createNarHierarchy(narInfos, narHierarchy);
// Create a new list and add the layers of the Stack from top to bottom which gets parents ahead of children
final List<NarPersistenceInfo> orderedNarInfos = new ArrayList<>(numNarInfos);
while (!narHierarchy.isEmpty()) {
final NarPersistenceInfo narInfoToAdd = narHierarchy.pop();
orderedNarInfos.add(narInfoToAdd);
}
logger.debug("NAR Infos after reordering: {}", orderedNarInfos);
return orderedNarInfos;
}
private void restoreState(final Collection<NarPersistenceInfo> narInfos) {
private void createNarHierarchy(final Collection<NarPersistenceInfo> narInfos, final Stack<NarPersistenceInfo> narHierarchy) {
if (narInfos == null || narInfos.isEmpty()) {
return;
}
// Create lookup from coordinate to NAR info
final Map<BundleCoordinate, NarPersistenceInfo> narInfosByBundleCoordinate = new HashMap<>();
narInfos.forEach(narInfo -> narInfosByBundleCoordinate.put(narInfo.getNarProperties().getCoordinate(), narInfo));
// Determine all the NARs that are parents of other NARs
final Set<NarPersistenceInfo> parentNarInfos = new HashSet<>();
for (final NarPersistenceInfo narInfo : narInfos) {
narInfo.getNarProperties().getDependencyCoordinate().ifPresent(parentCoordinate -> {
final NarPersistenceInfo parentNarInfo = narInfosByBundleCoordinate.get(parentCoordinate);
if (parentNarInfo != null) {
parentNarInfos.add(parentNarInfo);
}
});
}
// Remove the parent NARs from the current collection, and recurse on the parents
narInfos.removeAll(parentNarInfos);
// Add the remaining non-parent NARs to the hierarchy
narInfos.forEach(narHierarchy::push);
// Recurse on the parents NARs to further re-order them
createNarHierarchy(parentNarInfos, narHierarchy);
}
private void restoreNarNodes(final Collection<NarPersistenceInfo> narInfos) {
for (final NarPersistenceInfo narInfo : narInfos) {
try {
final NarNode narNode = restoreNarNode(narInfo);
narNodesById.put(narNode.getIdentifier(), narNode);
logger.debug("Restored NAR [{}] with state [{}] and identifier [{}]",
narNode.getManifest().getCoordinate(), narNode.getState(), narNode.getIdentifier());
final File narFile = narInfo.getNarFile();
final NarManifest manifest = NarManifest.fromNarFile(narFile);
final NarNode narNode = installNar(narInfo, manifest, false);
logger.debug("Restored NAR [{}] with state [{}] and identifier [{}]", narNode.getManifest().getCoordinate(), narNode.getState(), narNode.getIdentifier());
} catch (final Exception e) {
logger.warn("Failed to restore NAR for [{}]", narInfo.getNarFile().getAbsolutePath(), e);
}
}
}
private NarNode restoreNarNode(final NarPersistenceInfo narInfo) throws IOException {
final File narFile = narInfo.getNarFile();
final NarManifest manifest = NarManifest.fromNarFile(narFile);
final BundleCoordinate coordinate = manifest.getCoordinate();
final String identifier = createIdentifier(coordinate);
final NarState state = determineNarState(manifest);
final String narDigest = computeNarDigest(narFile);
return NarNode.builder()
.identifier(identifier)
.narFile(narFile)
.narFileDigest(narDigest)
.manifest(manifest)
.source(NarSource.valueOf(narInfo.getNarProperties().getSourceType()))
.sourceIdentifier(narInfo.getNarProperties().getSourceId())
.state(state)
.build();
}
private NarNode installNar(final NarInstallRequest installRequest, final boolean async) throws IOException {
final InputStream inputStream = installRequest.getInputStream();
final File tempNarFile = persistenceProvider.createTempFile(inputStream);
@ -410,11 +439,6 @@ public class StandardNarManager implements NarManager, InitializingBean, Closeab
throw new IllegalStateException("A NAR is already registered with the same coordinate [%s], and can not be replaced because it is not part of the NAR Manager".formatted(coordinate));
}
final Set<Bundle> bundlesWithMatchingDependency = extensionManager.getDependentBundles(coordinate);
if (!bundlesWithMatchingDependency.isEmpty()) {
throw new IllegalStateException("Unable to replace NAR [%s] because it is a dependency of other NARs".formatted(coordinate));
}
final NarPersistenceContext persistenceContext = NarPersistenceContext.builder()
.manifest(manifest)
.source(installRequest.getSource())
@ -423,8 +447,12 @@ public class StandardNarManager implements NarManager, InitializingBean, Closeab
.build();
final NarPersistenceInfo narPersistenceInfo = persistenceProvider.saveNar(persistenceContext, tempNarFile);
return installNar(narPersistenceInfo, manifest, async);
}
private NarNode installNar(final NarPersistenceInfo narPersistenceInfo, final NarManifest manifest, final boolean async) throws IOException {
final File narFile = narPersistenceInfo.getNarFile();
final BundleCoordinate coordinate = narPersistenceInfo.getNarProperties().getCoordinate();
final String identifier = createIdentifier(coordinate);
final String narDigest = computeNarDigest(narFile);
@ -433,8 +461,8 @@ public class StandardNarManager implements NarManager, InitializingBean, Closeab
.narFile(narFile)
.narFileDigest(narDigest)
.manifest(manifest)
.source(installRequest.getSource())
.sourceIdentifier(installRequest.getSourceIdentifier())
.source(NarSource.valueOf(narPersistenceInfo.getNarProperties().getSourceType()))
.sourceIdentifier(narPersistenceInfo.getNarProperties().getSourceId())
.state(NarState.WAITING_TO_INSTALL)
.build();
narNodesById.put(identifier, narNode);
@ -474,20 +502,6 @@ public class StandardNarManager implements NarManager, InitializingBean, Closeab
}
}
private NarState determineNarState(final NarManifest manifest) {
final BundleCoordinate coordinate = manifest.getCoordinate();
if (extensionManager.getBundle(coordinate) != null) {
return NarState.INSTALLED;
}
final BundleCoordinate dependencyCoordinate = manifest.getDependencyCoordinate();
if (dependencyCoordinate != null && extensionManager.getBundle(dependencyCoordinate) == null) {
return NarState.MISSING_DEPENDENCY;
}
return NarState.FAILED;
}
private String createIdentifier(final BundleCoordinate coordinate) {
return UUID.nameUUIDFromBytes(coordinate.getCoordinate().getBytes(StandardCharsets.UTF_8)).toString();
}

View File

@ -18,7 +18,7 @@ package org.apache.nifi.nar;
import org.apache.nifi.bundle.Bundle;
import java.util.Set;
import java.util.Collection;
/**
* Loads WARs from extensions and makes them available to the running application.
@ -28,10 +28,10 @@ public interface ExtensionUiLoader {
/**
* @param bundles the set of bundles to load WARs from
*/
void loadExtensionUis(Set<Bundle> bundles);
void loadExtensionUis(Collection<Bundle> bundles);
/**
* @param bundles the set of bundles to unload WARs for
*/
void unloadExtensionUis(Set<Bundle> bundles);
void unloadExtensionUis(Collection<Bundle> bundles);
}

View File

@ -49,7 +49,7 @@ public interface NarLoader {
*
* @param bundles the NARs to unload
*/
void unload(Set<Bundle> bundles);
void unload(Collection<Bundle> bundles);
/**
* Unloads the given NAR.

View File

@ -35,7 +35,6 @@ import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
/**
* Loads a set of NARs from the file system into the running application.
@ -142,15 +141,15 @@ public class StandardNarLoader implements NarLoader {
}
@Override
public synchronized void unload(final Set<Bundle> bundles) {
public synchronized void unload(final Collection<Bundle> bundles) {
if (extensionUiLoader != null) {
extensionUiLoader.unloadExtensionUis(bundles);
}
final Set<BundleCoordinate> bundleCoordinates = bundles.stream()
final List<BundleCoordinate> bundleCoordinates = bundles.stream()
.map(Bundle::getBundleDetails)
.map(BundleDetails::getCoordinate)
.collect(Collectors.toSet());
.toList();
for (final BundleCoordinate bundleCoordinate : bundleCoordinates) {
LOGGER.info("Unloading bundle [{}]", bundleCoordinate);

View File

@ -32,8 +32,8 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -92,12 +92,12 @@ public abstract class AbstractTestNarLoader {
extensionMapping,
new ExtensionUiLoader() {
@Override
public void loadExtensionUis(final Set<Bundle> bundles) {
public void loadExtensionUis(final Collection<Bundle> bundles) {
}
@Override
public void unloadExtensionUis(final Set<Bundle> bundles) {
public void unloadExtensionUis(final Collection<Bundle> bundles) {
}
},

View File

@ -22,6 +22,7 @@ import org.apache.nifi.components.ConfigurableComponent;
import org.apache.nifi.python.PythonProcessorDetails;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.Set;
@ -116,7 +117,7 @@ public interface ExtensionManager {
* @param bundleCoordinates the coordinates
* @return the removed bundles, or empty if none exists
*/
Set<Bundle> removeBundles(Set<BundleCoordinate> bundleCoordinates);
Set<Bundle> removeBundles(Collection<BundleCoordinate> bundleCoordinates);
/**
* Retrieves the bundles that have a dependency on the bundle with the given coordinate.

View File

@ -722,8 +722,8 @@ public class StandardExtensionDiscoveringManager implements ExtensionDiscovering
}
@Override
public synchronized Set<Bundle> removeBundles(final Set<BundleCoordinate> bundleCoordinates) {
final Set<Bundle> removedBundles = new HashSet<>();
public synchronized Set<Bundle> removeBundles(final Collection<BundleCoordinate> bundleCoordinates) {
final Set<Bundle> removedBundles = new LinkedHashSet<>();
for (final BundleCoordinate bundleCoordinate : bundleCoordinates) {
final Bundle removedBundle = removeBundle(bundleCoordinate);
if (removedBundle != null) {

View File

@ -337,7 +337,7 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
}
@Override
public synchronized void loadExtensionUis(final Set<Bundle> bundles) {
public synchronized void loadExtensionUis(final Collection<Bundle> bundles) {
extensionUisToLoad.addAll(bundles);
}
@ -393,7 +393,7 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
}
@Override
public synchronized void unloadExtensionUis(final Set<Bundle> bundles) {
public synchronized void unloadExtensionUis(final Collection<Bundle> bundles) {
bundles.forEach(this::unloadExtensionUis);
}

View File

@ -130,8 +130,9 @@ public class NarUploadStandaloneIT extends NiFiSystemIT {
assertNotNull(customProcessor.getComponent().getExtensionMissing());
assertFalse(customProcessor.getComponent().getExtensionMissing());
// Verify service API NAR can't be replaced while other NARs depend on it
assertThrows(NiFiClientException.class, () -> narUploadUtil.uploadNar(narsLocation, CONTROLLER_SERVICE_API_NAR_ID));
// Verify service API NAR can be replaced while other NARs depend on it
final NarSummaryDTO replacedServiceApiNar = narUploadUtil.uploadNar(narsLocation, CONTROLLER_SERVICE_API_NAR_ID);
waitFor(narUploadUtil.getWaitForNarStateSupplier(replacedServiceApiNar.getIdentifier(), NarState.INSTALLED));
// Verify processors NAR can be replaced
final NarSummaryDTO replacedProcessorsNar = narUploadUtil.uploadNar(narsLocation, PROCESSORS_NAR_ID);