From 38b04f03254ba0287a2a70c99181a6ca3fbc3a84 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 16 Jul 2020 10:11:49 +0200 Subject: Reapply "Load platform bundles separately" This reverts commit 0355cb740fe498abc03861bcb64de5e418c2fa88. --- .../core/config/ApplicationBundleLoader.java | 165 ++++++++++++++ .../yahoo/container/core/config/BundleManager.java | 236 --------------------- .../core/config/HandlersConfigurerDi.java | 18 +- .../core/config/PlatformBundleLoader.java | 45 ++++ .../testutil/HandlersConfigurerTestWrapper.java | 3 +- .../core/config/testutil/MockOsgiWrapper.java | 5 - .../src/main/java/com/yahoo/osgi/MockOsgi.java | 6 +- .../src/main/java/com/yahoo/osgi/Osgi.java | 6 +- .../src/main/java/com/yahoo/osgi/OsgiImpl.java | 12 +- 9 files changed, 238 insertions(+), 258 deletions(-) create mode 100644 container-core/src/main/java/com/yahoo/container/core/config/ApplicationBundleLoader.java delete mode 100644 container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java create mode 100644 container-core/src/main/java/com/yahoo/container/core/config/PlatformBundleLoader.java (limited to 'container-core/src/main') diff --git a/container-core/src/main/java/com/yahoo/container/core/config/ApplicationBundleLoader.java b/container-core/src/main/java/com/yahoo/container/core/config/ApplicationBundleLoader.java new file mode 100644 index 00000000000..e20fbd7b2ae --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/core/config/ApplicationBundleLoader.java @@ -0,0 +1,165 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.core.config; + +import com.yahoo.config.FileReference; +import com.yahoo.container.Container; +import com.yahoo.filedistribution.fileacquirer.FileAcquirer; +import com.yahoo.osgi.Osgi; +import org.osgi.framework.Bundle; +import org.osgi.framework.wiring.BundleRevision; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Manages the set of installed and active/inactive bundles. + * + * @author gjoranv + * @author Tony Vaagenes + */ +public class ApplicationBundleLoader { + + /* Map of file refs of active bundles (not scheduled for uninstall) to the installed bundle. + * + * Used to: + * 1. Avoid installing already installed bundles. Just an optimization, installing the same bundle location is a NOP + * 2. Start bundles (all are started every time) + * 3. Calculate the set of bundles to uninstall + */ + private final Map reference2Bundle = new LinkedHashMap<>(); + + private final Logger log = Logger.getLogger(ApplicationBundleLoader.class.getName()); + private final Osgi osgi; + + // A custom bundle installer for non-disk bundles, to be used for testing + private BundleInstaller customBundleInstaller = null; + + public ApplicationBundleLoader(Osgi osgi) { + this.osgi = osgi; + } + + /** + * Installs the given set of bundles and returns the set of bundles that is no longer used + * by the application, and should therefore be scheduled for uninstall. + */ + public synchronized Set useBundles(List newFileReferences) { + + Set obsoleteReferences = getObsoleteFileReferences(newFileReferences); + Set bundlesToUninstall = getObsoleteBundles(obsoleteReferences); + log.info("Bundles to schedule for uninstall: " + bundlesToUninstall); + + osgi.allowDuplicateBundles(bundlesToUninstall); + removeInactiveFileReferences(obsoleteReferences); + + installBundles(newFileReferences); + startBundles(); + log.info(installedBundlesMessage()); + + return bundlesToUninstall; + } + + private Set getObsoleteFileReferences(List newReferences) { + Set obsoleteReferences = new HashSet<>(reference2Bundle.keySet()); + obsoleteReferences.removeAll(newReferences); + return obsoleteReferences; + } + + + /** + * Returns the bundles that will not be retained by the new application generation. + */ + private Set getObsoleteBundles(Set obsoleteReferences) { + return obsoleteReferences.stream().map(reference2Bundle::get).collect(Collectors.toSet()); + } + + private void removeInactiveFileReferences(Set fileReferencesToRemove) { + fileReferencesToRemove.forEach(reference2Bundle::remove); + } + + private void installBundles(List references) { + Set bundlesToInstall = new HashSet<>(references); + + // This is just an optimization, as installing a bundle with the same location id returns the already installed bundle. + bundlesToInstall.removeAll(reference2Bundle.keySet()); + + if (!bundlesToInstall.isEmpty()) { + FileAcquirer fileAcquirer = Container.get().getFileAcquirer(); + boolean hasFileDistribution = (fileAcquirer != null); + if (hasFileDistribution) { + installWithFileDistribution(bundlesToInstall, new FileAcquirerBundleInstaller(fileAcquirer)); + } else if (customBundleInstaller != null) { + installWithFileDistribution(bundlesToInstall, customBundleInstaller); + } else { + log.warning("Can't retrieve bundles since file distribution is disabled."); + } + } + } + + private void installWithFileDistribution(Set bundlesToInstall, BundleInstaller bundleInstaller) { + for (FileReference reference : bundlesToInstall) { + try { + log.info("Installing bundle with reference '" + reference.value() + "'"); + List bundles = bundleInstaller.installBundles(reference, osgi); + + // Throw if more than one bundle was installed, which means that the X-JDisc-Preinstall-Bundle header was used. + // However, if the OSGi framework is only a test framework, this rule does not apply. + if (bundles.size() > 1 && osgi.hasFelixFramework()) { + throw new RuntimeException("Bundle '" + bundles.get(0).getSymbolicName() + "' tried to pre-install other bundles."); + } + reference2Bundle.put(reference, bundles.get(0)); + } + catch(Exception e) { + throw new RuntimeException("Could not install bundle with reference '" + reference + "'", e); + } + } + } + + /** + * Resolves and starts (calls the Bundles BundleActivator) all bundles. Bundle resolution must take place + * after all bundles are installed to ensure that the framework can resolve dependencies between bundles. + */ + private void startBundles() { + for (var bundle : reference2Bundle.values()) { + try { + if ( ! isFragment(bundle)) + bundle.start(); // NOP for already ACTIVE bundles + } catch(Exception e) { + throw new RuntimeException("Could not start bundle '" + bundle.getSymbolicName() + "'", e); + } + } + } + + private boolean isFragment(Bundle bundle) { + BundleRevision bundleRevision = bundle.adapt(BundleRevision.class); + if (bundleRevision == null) + throw new NullPointerException("Null bundle revision means that bundle has probably been uninstalled: " + + bundle.getSymbolicName() + ":" + bundle.getVersion()); + return (bundleRevision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0; + } + + private String installedBundlesMessage() { + StringBuilder sb = new StringBuilder("Installed bundles: {" ); + for (Bundle b : osgi.getBundles()) + sb.append("[" + b.getBundleId() + "]" + b.getSymbolicName() + ":" + b.getVersion() + ", "); + sb.setLength(sb.length() - 2); + sb.append("}"); + return sb.toString(); + } + + // Only for testing + void useCustomBundleInstaller(BundleInstaller bundleInstaller) { + customBundleInstaller = bundleInstaller; + } + + // Only for testing + List getActiveFileReferences() { + return new ArrayList<>(reference2Bundle.keySet()); + } + +} diff --git a/container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java b/container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java deleted file mode 100644 index 406d68408e3..00000000000 --- a/container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.core.config; - -import com.yahoo.collections.PredicateSplit; -import com.yahoo.config.FileReference; -import com.yahoo.container.Container; -import com.yahoo.filedistribution.fileacquirer.FileAcquirer; -import com.yahoo.osgi.Osgi; -import org.osgi.framework.Bundle; -import org.osgi.framework.wiring.BundleRevision; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import static com.yahoo.collections.PredicateSplit.partition; -import static com.yahoo.container.core.BundleLoaderProperties.DISK_BUNDLE_PREFIX; - -/** - * Manages the set of installed and active/inactive bundles. - * - * @author gjoranv - * @author Tony Vaagenes - */ -public class BundleManager { - - /* Map of file refs of active bundles (not scheduled for uninstall) to a list of all bundles that were installed - * (pre-install directive) by the bundle pointed to by the file ref (including itself). - * - * Used to: - * 1. Avoid installing already installed bundles. Just an optimization, installing the same bundle location is a NOP - * 2. Start bundles (all are started every time) - * 3. Calculate the set of bundles to uninstall - */ - private final Map> reference2Bundles = new LinkedHashMap<>(); - - private final Logger log = Logger.getLogger(BundleManager.class.getName()); - private final Osgi osgi; - - // A custom bundle installer for non-disk bundles, to be used for testing - private BundleInstaller customBundleInstaller = null; - - public BundleManager(Osgi osgi) { - this.osgi = osgi; - } - - /** - * Installs the given set of bundles and returns the set of bundles that is no longer used - * by the application, and should therefore be scheduled for uninstall. - */ - public synchronized Set use(List newFileReferences) { - // Must be done before allowing duplicates because allowed duplicates affect osgi.getCurrentBundles - Set bundlesToUninstall = getObsoleteBundles(newFileReferences); - - Set obsoleteReferences = getObsoleteFileReferences(newFileReferences); - allowDuplicateBundles(obsoleteReferences); - removeInactiveFileReferences(obsoleteReferences); - - installBundles(newFileReferences); - startBundles(); - - bundlesToUninstall.removeAll(allActiveBundles()); - log.info("Bundles to schedule for uninstall: " + bundlesToUninstall); - - log.info(installedBundlesMessage()); - return bundlesToUninstall; - } - - /** - * Returns the bundles that are not assumed to be retained by the new application generation. - * Note that at this point we don't yet know the full set of new bundles, because of the potential - * pre-install directives in the new bundles. However, only "disk bundles" (file:) can be listed - * in the pre-install directive, so we know about all the obsolete application bundles. - */ - private Set getObsoleteBundles(List newReferences) { - Set bundlesToRemove = new HashSet<>(osgi.getCurrentBundles()); - - for (FileReference fileReferenceToKeep : newReferences) { - if (reference2Bundles.containsKey(fileReferenceToKeep)) { - bundlesToRemove.removeAll(reference2Bundles.get(fileReferenceToKeep)); - } - } - bundlesToRemove.removeAll(osgi.getInitialBundles()); - return bundlesToRemove; - } - - - private Set getObsoleteFileReferences(List newReferences) { - Set obsoleteReferences = new HashSet<>(reference2Bundles.keySet()); - obsoleteReferences.removeAll(newReferences); - return obsoleteReferences; - } - - /** - * Allow duplicates (bsn+version) for each bundle that corresponds to obsolete file references, - * and avoid allowing duplicates for bundles that were installed via the - * X-JDisc-Preinstall-Bundle directive. These bundles are always "disk bundles" (library - * bundles installed on the node, and not transferred via file distribution). - * Such bundles will never have duplicates because they always have the same location id. - */ - private void allowDuplicateBundles(Set obsoleteReferences) { - // The bundle at index 0 for each file reference always corresponds to the bundle at the file reference location - Set allowedDuplicates = obsoleteReferences.stream() - .filter(reference -> ! isDiskBundle(reference)) - .map(reference -> reference2Bundles.get(reference).get(0)) - .collect(Collectors.toSet()); - - log.info(() -> allowedDuplicates.isEmpty() ? "" : "Adding bundles to allowed duplicates: " + allowedDuplicates); - osgi.allowDuplicateBundles(allowedDuplicates); - } - - /** - * Cleans up the map of active file references - */ - private void removeInactiveFileReferences(Set fileReferencesToRemove) { - // Clean up the map of active bundles - fileReferencesToRemove.forEach(reference2Bundles::remove); - } - - private void installBundles(List references) { - Set bundlesToInstall = new HashSet<>(references); - - // This is just an optimization, as installing a bundle with the same location id returns the already installed bundle. - bundlesToInstall.removeAll(reference2Bundles.keySet()); - - PredicateSplit bundlesToInstall_isDisk = partition(bundlesToInstall, BundleManager::isDiskBundle); - installBundlesFromDisk(bundlesToInstall_isDisk.trueValues); - installBundlesFromFileDistribution(bundlesToInstall_isDisk.falseValues); - } - - private static boolean isDiskBundle(FileReference fileReference) { - return fileReference.value().startsWith(DISK_BUNDLE_PREFIX); - } - - private void installBundlesFromDisk(List bundlesToInstall) { - for (FileReference reference : bundlesToInstall) { - try { - installBundleFromDisk(reference); - } - catch(Exception e) { - throw new RuntimeException("Could not install bundle '" + reference + "'", e); - } - } - } - - private void installBundlesFromFileDistribution(List bundlesToInstall) { - if (!bundlesToInstall.isEmpty()) { - FileAcquirer fileAcquirer = Container.get().getFileAcquirer(); - boolean hasFileDistribution = (fileAcquirer != null); - if (hasFileDistribution) { - installWithFileDistribution(bundlesToInstall, new FileAcquirerBundleInstaller(fileAcquirer)); - } else if (customBundleInstaller != null) { - installWithFileDistribution(bundlesToInstall, customBundleInstaller); - } else { - log.warning("Can't retrieve bundles since file distribution is disabled."); - } - } - } - - private void installBundleFromDisk(FileReference reference) { - log.info("Installing bundle from disk with reference '" + reference.value() + "'"); - - var bundleInstaller = new DiskBundleInstaller(); - List bundles = bundleInstaller.installBundles(reference, osgi); - reference2Bundles.put(reference, bundles); - } - - private void installWithFileDistribution(List bundlesToInstall, BundleInstaller bundleInstaller) { - for (FileReference reference : bundlesToInstall) { - try { - log.info("Installing bundle with reference '" + reference.value() + "'"); - List bundles = bundleInstaller.installBundles(reference, osgi); - reference2Bundles.put(reference, bundles); - } - catch(Exception e) { - throw new RuntimeException("Could not install bundle '" + reference + "'", e); - } - } - } - - /** - * Resolves and starts (calls the Bundles BundleActivator) all bundles. Bundle resolution must take place - * after all bundles are installed to ensure that the framework can resolve dependencies between bundles. - */ - private void startBundles() { - for (List bundles : reference2Bundles.values()) { - for (Bundle bundle : bundles) { - try { - if ( ! isFragment(bundle)) - bundle.start(); // NOP for already ACTIVE bundles - } catch(Exception e) { - throw new RuntimeException("Could not start bundle '" + bundle.getSymbolicName() + "'", e); - } - } - } - } - - private boolean isFragment(Bundle bundle) { - BundleRevision bundleRevision = bundle.adapt(BundleRevision.class); - if (bundleRevision == null) - throw new NullPointerException("Null bundle revision means that bundle has probably been uninstalled: " + - bundle.getSymbolicName() + ":" + bundle.getVersion()); - return (bundleRevision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0; - } - - private Set allActiveBundles() { - return reference2Bundles.keySet().stream() - .flatMap(reference -> reference2Bundles.get(reference).stream()) - .collect(Collectors.toSet()); - } - - private String installedBundlesMessage() { - StringBuilder sb = new StringBuilder("Installed bundles: {" ); - for (Bundle b : osgi.getBundles()) - sb.append("[" + b.getBundleId() + "]" + b.getSymbolicName() + ":" + b.getVersion() + ", "); - sb.setLength(sb.length() - 2); - sb.append("}"); - return sb.toString(); - } - - // Only for testing - void useCustomBundleInstaller(BundleInstaller bundleInstaller) { - customBundleInstaller = bundleInstaller; - } - - // Only for testing - List getActiveFileReferences() { - return new ArrayList<>(reference2Bundles.keySet()); - } - -} diff --git a/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java b/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java index b983cbcfe37..40470a9f096 100644 --- a/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java +++ b/container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java @@ -101,12 +101,16 @@ public class HandlersConfigurerDi { private static class ContainerAndDiOsgi extends OsgiImpl implements OsgiWrapper { private final OsgiFramework osgiFramework; - private final BundleManager bundleManager; + private final ApplicationBundleLoader applicationBundleLoader; + private final PlatformBundleLoader platformBundleLoader; public ContainerAndDiOsgi(OsgiFramework osgiFramework) { super(osgiFramework); this.osgiFramework = osgiFramework; - bundleManager = new BundleManager(new OsgiImpl(osgiFramework)); + + OsgiImpl osgi = new OsgiImpl(osgiFramework); + applicationBundleLoader = new ApplicationBundleLoader(osgi); + platformBundleLoader = new PlatformBundleLoader(osgi); } @@ -131,9 +135,15 @@ public class HandlersConfigurerDi { } @Override - public Set useBundles(Collection bundles) { + public void installPlatformBundles(Collection bundles) { + log.fine("Installing platform bundles."); + platformBundleLoader.install(bundles); + } + + @Override + public Set useApplicationBundles(Collection bundles) { log.info("Installing bundles from the latest application"); - return bundleManager.use(new ArrayList<>(bundles)); + return applicationBundleLoader.useBundles(new ArrayList<>(bundles)); } } diff --git a/container-core/src/main/java/com/yahoo/container/core/config/PlatformBundleLoader.java b/container-core/src/main/java/com/yahoo/container/core/config/PlatformBundleLoader.java new file mode 100644 index 00000000000..76f0b959a58 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/core/config/PlatformBundleLoader.java @@ -0,0 +1,45 @@ +package com.yahoo.container.core.config; + +import com.yahoo.config.FileReference; +import com.yahoo.osgi.Osgi; +import org.osgi.framework.Bundle; + +import java.util.Collection; +import java.util.List; +import java.util.logging.Logger; + +/** + * Installs all platform bundles, using the {@link DiskBundleInstaller}. + * All platform bundles reside on disk, and they are never uninstalled. + * + * @author gjoranv + */ +public class PlatformBundleLoader { + private static final Logger log = Logger.getLogger(PlatformBundleLoader.class.getName()); + + private final Osgi osgi; + private final DiskBundleInstaller installer; + + public PlatformBundleLoader(Osgi osgi) { + this.osgi = osgi; + installer = new DiskBundleInstaller(); + } + + public void install(Collection bundlesToInstall) { + for (FileReference reference : bundlesToInstall) { + try { + installBundleFromDisk(reference); + } + catch(Exception e) { + throw new RuntimeException("Could not install bundle '" + reference + "'", e); + } + } + } + + private void installBundleFromDisk(FileReference reference) { + log.info("Installing bundle from disk with reference '" + reference.value() + "'"); + List bundles = installer.installBundles(reference, osgi); + log.fine("Installed " + bundles.size() + " bundles for file reference " + reference); + } + +} diff --git a/container-core/src/main/java/com/yahoo/container/core/config/testutil/HandlersConfigurerTestWrapper.java b/container-core/src/main/java/com/yahoo/container/core/config/testutil/HandlersConfigurerTestWrapper.java index 9f49b016b68..503bf2f2db1 100644 --- a/container-core/src/main/java/com/yahoo/container/core/config/testutil/HandlersConfigurerTestWrapper.java +++ b/container-core/src/main/java/com/yahoo/container/core/config/testutil/HandlersConfigurerTestWrapper.java @@ -41,7 +41,8 @@ public class HandlersConfigurerTestWrapper { private final static String testFiles[] = { "components.cfg", "handlers.cfg", - "bundles.cfg", + "platform-bundles.cfg", + "application-bundles.cfg", "string.cfg", "int.cfg", "renderers.cfg", diff --git a/container-core/src/main/java/com/yahoo/container/core/config/testutil/MockOsgiWrapper.java b/container-core/src/main/java/com/yahoo/container/core/config/testutil/MockOsgiWrapper.java index ac0fbd71671..98c927b8efd 100644 --- a/container-core/src/main/java/com/yahoo/container/core/config/testutil/MockOsgiWrapper.java +++ b/container-core/src/main/java/com/yahoo/container/core/config/testutil/MockOsgiWrapper.java @@ -15,11 +15,6 @@ import static java.util.Collections.emptyList; */ public class MockOsgiWrapper implements OsgiWrapper { - @Override - public List getInitialBundles() { - return emptyList(); - } - @Override public Bundle[] getBundles() { return new Bundle[0]; diff --git a/container-core/src/main/java/com/yahoo/osgi/MockOsgi.java b/container-core/src/main/java/com/yahoo/osgi/MockOsgi.java index d809c493565..6a700a65a03 100644 --- a/container-core/src/main/java/com/yahoo/osgi/MockOsgi.java +++ b/container-core/src/main/java/com/yahoo/osgi/MockOsgi.java @@ -12,14 +12,10 @@ import java.util.List; /** * @author Tony Vaagenes + * @author gjoranv */ public class MockOsgi extends NonWorkingOsgiFramework implements Osgi { - @Override - public List getInitialBundles() { - return Collections.emptyList(); - } - @Override public Bundle[] getBundles() { return new Bundle[0]; diff --git a/container-core/src/main/java/com/yahoo/osgi/Osgi.java b/container-core/src/main/java/com/yahoo/osgi/Osgi.java index 8f0acf41f30..513e7883594 100644 --- a/container-core/src/main/java/com/yahoo/osgi/Osgi.java +++ b/container-core/src/main/java/com/yahoo/osgi/Osgi.java @@ -9,11 +9,10 @@ import java.util.List; /** * @author Tony Vaagenes + * @author gjoranv */ public interface Osgi { - List getInitialBundles(); - Bundle[] getBundles(); /** Returns all bundles that have not been scheduled for uninstall. */ @@ -25,4 +24,7 @@ public interface Osgi { void allowDuplicateBundles(Collection bundles); + default boolean hasFelixFramework() { + return false; + } } diff --git a/container-core/src/main/java/com/yahoo/osgi/OsgiImpl.java b/container-core/src/main/java/com/yahoo/osgi/OsgiImpl.java index ed93d15c975..998273acfc7 100644 --- a/container-core/src/main/java/com/yahoo/osgi/OsgiImpl.java +++ b/container-core/src/main/java/com/yahoo/osgi/OsgiImpl.java @@ -5,6 +5,7 @@ import com.yahoo.component.ComponentSpecification; import com.yahoo.component.Version; import com.yahoo.container.bundle.BundleInstantiationSpecification; import com.yahoo.jdisc.application.OsgiFramework; +import com.yahoo.jdisc.core.FelixFramework; import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; import org.osgi.framework.launch.Framework; @@ -16,6 +17,7 @@ import java.util.logging.Logger; /** * @author Tony Vaagenes * @author bratseth + * @author gjoranv */ public class OsgiImpl implements Osgi { private static final Logger log = Logger.getLogger(OsgiImpl.class.getName()); @@ -41,11 +43,6 @@ public class OsgiImpl implements Osgi { log.info("Using " + alwaysCurrentBundle + " to lookup current bundles."); } - @Override - public List getInitialBundles() { - return initialBundles; - } - @Override public Bundle[] getBundles() { List bundles = jdiscOsgi.bundles(); @@ -155,6 +152,11 @@ public class OsgiImpl implements Osgi { jdiscOsgi.allowDuplicateBundles(bundles); } + @Override + public boolean hasFelixFramework() { + return jdiscOsgi instanceof FelixFramework; + } + private static Bundle firstNonFrameworkBundle(List bundles) { for (Bundle b : bundles) { if (! (b instanceof Framework)) -- cgit v1.2.3