diff options
Diffstat (limited to 'container-core')
35 files changed, 1337 insertions, 228 deletions
diff --git a/container-core/CMakeLists.txt b/container-core/CMakeLists.txt index 341155457a8..6132c253c13 100644 --- a/container-core/CMakeLists.txt +++ b/container-core/CMakeLists.txt @@ -8,6 +8,7 @@ install_config_definition(src/main/resources/configdefinitions/identity.def cont install_config_definition(src/main/resources/configdefinitions/log-handler.def container.core.log-handler.def) install_config_definition(src/main/resources/configdefinitions/metrics-packets-handler.def container.jdisc.state.metrics-packets-handler.def) install_config_definition(src/main/resources/configdefinitions/metrics-presentation.def metrics.metrics-presentation.def) +install_config_definition(src/main/resources/configdefinitions/metrics-proxy-api.def container.handler.metrics.metrics-proxy-api.def) install_config_definition(src/main/resources/configdefinitions/mockservice.def container.handler.test.mockservice.def) install_config_definition(src/main/resources/configdefinitions/qr-searchers.def container.qr-searchers.def) install_config_definition(src/main/resources/configdefinitions/qr.def container.qr.def) diff --git a/container-core/abi-spec.json b/container-core/abi-spec.json index 91c0e4cc6bf..6d683c53984 100644 --- a/container-core/abi-spec.json +++ b/container-core/abi-spec.json @@ -11,6 +11,23 @@ ], "fields": [] }, + "com.yahoo.container.handler.ClustersStatus$Require": { + "superClass": "java.lang.Enum", + "interfaces": [], + "attributes": [ + "public", + "final", + "enum" + ], + "methods": [ + "public static com.yahoo.container.handler.ClustersStatus$Require[] values()", + "public static com.yahoo.container.handler.ClustersStatus$Require valueOf(java.lang.String)" + ], + "fields": [ + "public static final enum com.yahoo.container.handler.ClustersStatus$Require ONE", + "public static final enum com.yahoo.container.handler.ClustersStatus$Require ALL" + ] + }, "com.yahoo.container.handler.ClustersStatus": { "superClass": "com.yahoo.component.AbstractComponent", "interfaces": [], @@ -23,7 +40,8 @@ "public void setReceiveTrafficByDefault(boolean)", "public void setUp(java.lang.Object)", "public void setDown(java.lang.Object)", - "public boolean containerShouldReceiveTraffic()" + "public boolean containerShouldReceiveTraffic()", + "public boolean containerShouldReceiveTraffic(com.yahoo.container.handler.ClustersStatus$Require)" ], "fields": [] }, @@ -154,6 +172,7 @@ "public void <init>(com.yahoo.container.handler.ThreadpoolConfig)", "public com.yahoo.container.handler.ThreadpoolConfig$Builder maxthreads(int)", "public com.yahoo.container.handler.ThreadpoolConfig$Builder maxThreadExecutionTimeSeconds(int)", + "public com.yahoo.container.handler.ThreadpoolConfig$Builder softStartSeconds(double)", "public final boolean dispatchGetConfig(com.yahoo.config.ConfigInstance$Producer)", "public final java.lang.String getDefMd5()", "public final java.lang.String getDefName()", @@ -191,7 +210,8 @@ "public static java.lang.String getDefVersion()", "public void <init>(com.yahoo.container.handler.ThreadpoolConfig$Builder)", "public int maxthreads()", - "public int maxThreadExecutionTimeSeconds()" + "public int maxThreadExecutionTimeSeconds()", + "public double softStartSeconds()" ], "fields": [ "public static final java.lang.String CONFIG_DEF_MD5", @@ -230,6 +250,7 @@ "public void <init>(com.yahoo.container.QrSearchersConfig)", "public void <init>(com.yahoo.container.handler.ClustersStatus)", "public void <init>(com.yahoo.container.QrSearchersConfig, com.yahoo.container.handler.ClustersStatus)", + "public void <init>(com.yahoo.container.QrSearchersConfig, com.yahoo.container.core.VipStatusConfig, com.yahoo.container.handler.ClustersStatus, com.yahoo.container.jdisc.state.StateMonitor)", "public void <init>(com.yahoo.container.QrSearchersConfig, com.yahoo.container.handler.ClustersStatus, com.yahoo.container.jdisc.state.StateMonitor)", "public void <init>(com.yahoo.container.QrSearchersConfig, com.yahoo.container.core.VipStatusConfig, com.yahoo.container.handler.ClustersStatus)", "public void setInRotation(java.lang.Boolean)", @@ -853,5 +874,55 @@ "public bridge synthetic java.lang.Object clone()" ], "fields": [] + }, + "ai.vespa.cloud.Environment": { + "superClass": "java.lang.Enum", + "interfaces": [], + "attributes": [ + "public", + "final", + "enum" + ], + "methods": [ + "public static ai.vespa.cloud.Environment[] values()", + "public static ai.vespa.cloud.Environment valueOf(java.lang.String)" + ], + "fields": [ + "public static final enum ai.vespa.cloud.Environment dev", + "public static final enum ai.vespa.cloud.Environment perf", + "public static final enum ai.vespa.cloud.Environment test", + "public static final enum ai.vespa.cloud.Environment staging", + "public static final enum ai.vespa.cloud.Environment prod" + ] + }, + "ai.vespa.cloud.SystemInfo": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>(com.yahoo.cloud.config.ConfigserverConfig)", + "public void <init>(ai.vespa.cloud.Zone)", + "public ai.vespa.cloud.Zone zone()" + ], + "fields": [] + }, + "ai.vespa.cloud.Zone": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>(ai.vespa.cloud.Environment, java.lang.String)", + "public ai.vespa.cloud.Environment environment()", + "public java.lang.String region()", + "public java.lang.String toString()", + "public int hashCode()", + "public boolean equals(java.lang.Object)", + "public static ai.vespa.cloud.Zone from(java.lang.String)" + ], + "fields": [] } }
\ No newline at end of file diff --git a/container-core/pom.xml b/container-core/pom.xml index f3861c92129..64e5ebb00d3 100644 --- a/container-core/pom.xml +++ b/container-core/pom.xml @@ -16,6 +16,16 @@ <packaging>container-plugin</packaging> <dependencies> <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>http-utils</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + </dependency> + + <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <scope>provided</scope> @@ -227,6 +237,16 @@ <version>${project.version}</version> <scope>test</scope> </dependency> + <dependency> + <groupId>com.github.tomakehurst</groupId> + <artifactId>wiremock-standalone</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> <plugins> diff --git a/container-core/src/main/java/ai/vespa/cloud/Environment.java b/container-core/src/main/java/ai/vespa/cloud/Environment.java new file mode 100644 index 00000000000..8f1d9fc962a --- /dev/null +++ b/container-core/src/main/java/ai/vespa/cloud/Environment.java @@ -0,0 +1,13 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.cloud; + +/** + * The environments of a Vespa cloud instance + * + * @author bratseth + */ +public enum Environment { + + dev, perf, test, staging, prod + +} diff --git a/container-core/src/main/java/ai/vespa/cloud/SystemInfo.java b/container-core/src/main/java/ai/vespa/cloud/SystemInfo.java new file mode 100644 index 00000000000..0524ae072cd --- /dev/null +++ b/container-core/src/main/java/ai/vespa/cloud/SystemInfo.java @@ -0,0 +1,31 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.cloud; + +import com.google.inject.Inject; +import com.yahoo.cloud.config.ConfigserverConfig; + +/** + * Provides information about the system in which this container is running. + * This is available and can be injected when running in a cloud environment. + * + * @author bratseth + */ +public class SystemInfo { + + private final Zone zone; + + /** Do not use */ + @Inject + public SystemInfo(ConfigserverConfig config) { + this.zone = new Zone(Environment.valueOf(config.environment()), config.region()); + } + + /** Create an instance for testing */ + public SystemInfo(Zone zone) { + this.zone = zone; + } + + /** Returns the zone this is running in */ + public Zone zone() { return zone; } + +} diff --git a/container-core/src/main/java/ai/vespa/cloud/Zone.java b/container-core/src/main/java/ai/vespa/cloud/Zone.java new file mode 100644 index 00000000000..48293aa7908 --- /dev/null +++ b/container-core/src/main/java/ai/vespa/cloud/Zone.java @@ -0,0 +1,61 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.cloud; + +import java.util.Objects; + +/** + * The zone in which a cloud deployment may be running. + * A zone is a combination of an environment and a region. + * + * @author bratseth + */ +public class Zone { + + private final Environment environment; + + private final String region; + + public Zone(Environment environment, String region) { + this.environment = environment; + this.region = region; + } + + public Environment environment() { return environment; } + public String region() { return region; } + + /** Returns the string environment.region */ + @Override + public String toString() { return environment + "." + region; } + + @Override + public int hashCode() { return Objects.hash(environment, region); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if ( ! (o instanceof Zone)) return false; + Zone other = (Zone)o; + return this.environment.equals(other.environment) && this.region.equals(other.region); + } + + /** + * Creates a zone from a string on the form environment.region + * + * @throws IllegalArgumentException if the given string is not a valid zone + */ + public static Zone from(String zoneString) { + String[] parts = zoneString.split("\\."); + if (parts.length != 2) + throw new IllegalArgumentException("A zone string must be on the form [environment].[region], but was '" + zoneString + "'"); + + Environment environment; + try { + environment = Environment.valueOf(parts[0]); + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid zone '" + zoneString + "': No environment named '" + parts[0] + "'"); + } + return new Zone(environment, parts[1]); + } + +} diff --git a/container-core/src/main/java/ai/vespa/cloud/package-info.java b/container-core/src/main/java/ai/vespa/cloud/package-info.java new file mode 100644 index 00000000000..259a2bda258 --- /dev/null +++ b/container-core/src/main/java/ai/vespa/cloud/package-info.java @@ -0,0 +1,10 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * Public API to the Vespa cloud, available when this container runs in a cloud. + */ +@ExportPackage +@PublicApi +package ai.vespa.cloud; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-core/src/main/java/com/yahoo/container/Server.java b/container-core/src/main/java/com/yahoo/container/Server.java deleted file mode 100644 index a4dec6de5a2..00000000000 --- a/container-core/src/main/java/com/yahoo/container/Server.java +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container; - -import com.yahoo.config.subscription.ConfigSubscriber; -import com.yahoo.container.QrConfig.Rpc; - -/** - * The http server singleton managing listeners for various ports, - * and the threads used to respond to requests on the ports - * - * @author bratseth - * @deprecated - */ -@SuppressWarnings("deprecation") -@Deprecated // TODO: Remove this when the last usage og getServerDiscriminator is removed -public class Server { - - //TODO: Make this final again. - private static final Server instance = new Server(); - - /** A short string which is different for all the qrserver instances on a given node. */ - private String localServerDiscriminator = "qrserver.0"; - - private Server() { } - - public static Server get() { - return instance; - } - - public void initialize(QrConfig config) { - localServerDiscriminator = config.discriminator(); - } - - /** - * A string unique for this QRS on this server. - * - * @return a server specific string - * @deprecated do not use - */ - @Deprecated - public String getServerDiscriminator() { - return localServerDiscriminator; - } - -} diff --git a/container-core/src/main/java/com/yahoo/container/core/config/BundleInstaller.java b/container-core/src/main/java/com/yahoo/container/core/config/BundleInstaller.java new file mode 100644 index 00000000000..fc919571b6c --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/core/config/BundleInstaller.java @@ -0,0 +1,21 @@ +// 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.osgi.Osgi; +import org.osgi.framework.Bundle; + +import java.util.List; + +/** + * @author gjoranv + */ +public interface BundleInstaller { + + /** + * Installs the bundle with the given file reference, plus all bundles in its X-JDisc-Preinstall-Bundle directive. + * Returns all bundles installed to the given OSGi framework as a result of this call. + */ + List<Bundle> installBundles(FileReference reference, Osgi osgi) throws InterruptedException; + +} diff --git a/container-core/src/main/java/com/yahoo/container/core/config/BundleLoader.java b/container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java index 4c3d76436dd..5e9b42a5fda 100644 --- a/container-core/src/main/java/com/yahoo/container/core/config/BundleLoader.java +++ b/container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java @@ -1,4 +1,4 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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; @@ -9,13 +9,12 @@ import com.yahoo.osgi.Osgi; import org.osgi.framework.Bundle; import org.osgi.framework.wiring.BundleRevision; -import java.io.File; +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.concurrent.TimeUnit; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -23,12 +22,12 @@ import static com.yahoo.collections.PredicateSplit.partition; import static com.yahoo.container.core.BundleLoaderProperties.DISK_BUNDLE_PREFIX; /** - * Manages the set of installed 3rd-party component bundles. + * Manages the set of installed and active/inactive bundles. * - * @author Tony Vaagenes * @author gjoranv + * @author Tony Vaagenes */ -public class BundleLoader { +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). @@ -40,20 +39,96 @@ public class BundleLoader { */ private final Map<FileReference, List<Bundle>> reference2Bundles = new LinkedHashMap<>(); - private final Logger log = Logger.getLogger(BundleLoader.class.getName()); + private final Logger log = Logger.getLogger(BundleManager.class.getName()); private final Osgi osgi; - public BundleLoader(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; } - private void install(List<FileReference> references) { + /** + * 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<Bundle> use(List<FileReference> newFileReferences) { + // Must be done before allowing duplicates because allowed duplicates affect osgi.getCurrentBundles + Set<Bundle> bundlesToUninstall = getObsoleteBundles(newFileReferences); + + Set<FileReference> 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<Bundle> getObsoleteBundles(List<FileReference> newReferences) { + Set<Bundle> 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<FileReference> getObsoleteFileReferences(List<FileReference> newReferences) { + Set<FileReference> 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<FileReference> obsoleteReferences) { + // The bundle at index 0 for each file reference always corresponds to the bundle at the file reference location + Set<Bundle> 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<FileReference> fileReferencesToRemove) { + // Clean up the map of active bundles + fileReferencesToRemove.forEach(reference2Bundles::remove); + } + + private void installBundles(List<FileReference> references) { Set<FileReference> 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<FileReference> bundlesToInstall_isDisk = partition(bundlesToInstall, BundleLoader::isDiskBundle); + PredicateSplit<FileReference> bundlesToInstall_isDisk = partition(bundlesToInstall, BundleManager::isDiskBundle); installBundlesFromDisk(bundlesToInstall_isDisk.trueValues); installBundlesFromFileDistribution(bundlesToInstall_isDisk.falseValues); @@ -81,7 +156,9 @@ public class BundleLoader { FileAcquirer fileAcquirer = Container.get().getFileAcquirer(); boolean hasFileDistribution = (fileAcquirer != null); if (hasFileDistribution) { - installWithFileDistribution(bundlesToInstall, fileAcquirer); + installWithFileDistribution(bundlesToInstall, new FileAcquirerBundleInstaller(fileAcquirer)); + } else if (customBundleInstaller != null) { + installWithFileDistribution(bundlesToInstall, customBundleInstaller); } else { log.warning("Can't retrieve bundles since file distribution is disabled."); } @@ -89,25 +166,18 @@ public class BundleLoader { } private void installBundleFromDisk(FileReference reference) { - assert(reference.value().startsWith(DISK_BUNDLE_PREFIX)); - String referenceFileName = reference.value().substring(DISK_BUNDLE_PREFIX.length()); log.info("Installing bundle from disk with reference '" + reference.value() + "'"); - File file = new File(referenceFileName); - if ( ! file.exists()) { - throw new IllegalArgumentException("Reference '" + reference.value() + "' not found on disk."); - } - - List<Bundle> bundles = osgi.install(file.getAbsolutePath()); - + var bundleInstaller = new DiskBundleInstaller(); + List<Bundle> bundles = bundleInstaller.installBundles(reference, osgi); reference2Bundles.put(reference, bundles); } - private void installWithFileDistribution(List<FileReference> bundlesToInstall, FileAcquirer fileAcquirer) { + private void installWithFileDistribution(List<FileReference> bundlesToInstall, BundleInstaller bundleInstaller) { for (FileReference reference : bundlesToInstall) { try { log.info("Installing bundle with reference '" + reference.value() + "'"); - List<Bundle> bundles = obtainBundles(reference, fileAcquirer); + List<Bundle> bundles = bundleInstaller.installBundles(reference, osgi); reference2Bundles.put(reference, bundles); } catch(Exception e) { @@ -116,11 +186,6 @@ public class BundleLoader { } } - private List<Bundle> obtainBundles(FileReference reference, FileAcquirer fileAcquirer) throws InterruptedException { - File file = fileAcquirer.waitFor(reference, 7, TimeUnit.DAYS); - return osgi.install(file.getAbsolutePath()); - } - /** * 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. @@ -146,84 +211,12 @@ public class BundleLoader { return (bundleRevision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0; } - /** - * Returns the bundles that are not assumed to be retained by the new application generation. - * and cleans up the map of active file references. 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<Bundle> getObsoleteBundles(List<FileReference> newReferences) { - Set<Bundle> 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 void removeInactiveFileReferences(List<FileReference> newReferences) { - // Clean up the map of active bundles - Set<FileReference> fileReferencesToRemove = getObsoleteFileReferences(newReferences); - fileReferencesToRemove.forEach(reference2Bundles::remove); - } - - - /** - * 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(List<FileReference> newReferences) { - Set<FileReference> obsoleteReferences = getObsoleteFileReferences(newReferences); - - // The bundle at index 0 for each file reference always corresponds to the bundle at the file reference location - Set<Bundle> allowedDuplicates = obsoleteReferences.stream() - .map(reference -> reference2Bundles.get(reference).get(0)) - .collect(Collectors.toSet()); - - log.info(() -> allowedDuplicates.isEmpty() ? "" : "Adding bundles to allowed duplicates: " + allowedDuplicates); - osgi.allowDuplicateBundles(allowedDuplicates); - } - - private Set<FileReference> getObsoleteFileReferences(List<FileReference> newReferences) { - Set<FileReference> obsoleteReferences = new HashSet<>(reference2Bundles.keySet()); - obsoleteReferences.removeAll(newReferences); - return obsoleteReferences; - } - private Set<Bundle> allActiveBundles() { return reference2Bundles.keySet().stream() .flatMap(reference -> reference2Bundles.get(reference).stream()) .collect(Collectors.toSet()); } - /** - * 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<Bundle> use(List<FileReference> newBundles) { - // Must be done before allowing duplicates because allowed duplicates affect osgi.getCurrentBundles - Set<Bundle> bundlesToUninstall = getObsoleteBundles(newBundles); - - allowDuplicateBundles(newBundles); - removeInactiveFileReferences(newBundles); - - install(newBundles); - startBundles(); - - bundlesToUninstall.removeAll(allActiveBundles()); - log.info("Bundles to schedule for uninstall: " + bundlesToUninstall); - - log.info(installedBundlesMessage()); - return bundlesToUninstall; - } - private String installedBundlesMessage() { StringBuilder sb = new StringBuilder("Installed bundles: {" ); for (Bundle b : osgi.getBundles()) @@ -233,4 +226,14 @@ public class BundleLoader { return sb.toString(); } + // Only for testing + void useCustomBundleInstaller(BundleInstaller bundleInstaller) { + customBundleInstaller = bundleInstaller; + } + + // Only for testing + List<FileReference> getActiveFileReferences() { + return new ArrayList<>(reference2Bundles.keySet()); + } + } diff --git a/container-core/src/main/java/com/yahoo/container/core/config/DiskBundleInstaller.java b/container-core/src/main/java/com/yahoo/container/core/config/DiskBundleInstaller.java new file mode 100644 index 00000000000..3edabe9f861 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/core/config/DiskBundleInstaller.java @@ -0,0 +1,31 @@ +// 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.osgi.Osgi; +import org.osgi.framework.Bundle; + +import java.io.File; +import java.util.List; + +import static com.yahoo.container.core.BundleLoaderProperties.DISK_BUNDLE_PREFIX; + +/** + * @author gjoranv + */ +public class DiskBundleInstaller implements BundleInstaller { + + @Override + public List<Bundle> installBundles(FileReference reference, Osgi osgi) { + assert(reference.value().startsWith(DISK_BUNDLE_PREFIX)); + String referenceFileName = reference.value().substring(DISK_BUNDLE_PREFIX.length()); + + File file = new File(referenceFileName); + if ( ! file.exists()) { + throw new IllegalArgumentException("Reference '" + reference.value() + "' not found on disk."); + } + + return osgi.install(file.getAbsolutePath()); + } + +} diff --git a/container-core/src/main/java/com/yahoo/container/core/config/FileAcquirerBundleInstaller.java b/container-core/src/main/java/com/yahoo/container/core/config/FileAcquirerBundleInstaller.java new file mode 100644 index 00000000000..72951e67b4e --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/core/config/FileAcquirerBundleInstaller.java @@ -0,0 +1,53 @@ +// 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.filedistribution.fileacquirer.FileAcquirer; +import com.yahoo.osgi.Osgi; +import org.osgi.framework.Bundle; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * @author gjoranv + */ +public class FileAcquirerBundleInstaller implements BundleInstaller { + private static Logger log = Logger.getLogger(FileAcquirerBundleInstaller.class.getName()); + + private final FileAcquirer fileAcquirer; + + public FileAcquirerBundleInstaller(FileAcquirer fileAcquirer) { + this.fileAcquirer = fileAcquirer; + } + + @Override + public List<Bundle> installBundles(FileReference reference, Osgi osgi) throws InterruptedException { + File file = fileAcquirer.waitFor(reference, 7, TimeUnit.DAYS); + + if (notReadable(file)) { + // Wait a few sec in case FileAcquirer returns right before the file is actually ready. + // This happened on rare occasions due to a (fixed) bug in file distribution. + log.warning("Unable to open bundle file with reference '" + reference + "'. Waiting for up to 5 sec."); + int retries = 0; + while (notReadable(file) && retries < 10) { + Thread.sleep(500); + retries++; + } + if (notReadable(file)) { + com.yahoo.protect.Process.logAndDie("Shutting down - unable to read bundle file with reference '" + reference + + "' and path " + file.getAbsolutePath()); + } + } + + return osgi.install(file.getAbsolutePath()); + } + + private static boolean notReadable(File file) { + return ! Files.isReadable(file.toPath()); + } + +} 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 ef132694e10..d87b38e8b18 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 @@ -9,7 +9,6 @@ import com.yahoo.component.ComponentSpecification; import com.yahoo.component.provider.ComponentRegistry; import com.yahoo.concurrent.ThreadFactoryFactory; import com.yahoo.config.FileReference; -import com.yahoo.container.core.config.testutil.MockOsgiWrapper; import com.yahoo.container.di.ComponentDeconstructor; import com.yahoo.container.di.Container; import com.yahoo.container.di.componentgraph.core.ComponentGraph; @@ -82,7 +81,7 @@ public class HandlersConfigurerDi { OsgiFramework osgiFramework) { this(subscriberFactory, vespaContainer, configId, deconstructor, discInjector, - new ContainerAndDiOsgi(osgiFramework, new BundleLoader(new OsgiImpl(osgiFramework)))); + new ContainerAndDiOsgi(osgiFramework, new BundleManager(new OsgiImpl(osgiFramework)))); } // Only public for testing @@ -102,9 +101,9 @@ public class HandlersConfigurerDi { private static class ContainerAndDiOsgi extends OsgiImpl implements OsgiWrapper { private final OsgiFramework osgiFramework; - private final BundleLoader bundleLoader; + private final BundleManager bundleLoader; - public ContainerAndDiOsgi(OsgiFramework osgiFramework, BundleLoader bundleLoader) { + public ContainerAndDiOsgi(OsgiFramework osgiFramework, BundleManager bundleLoader) { super(osgiFramework); this.osgiFramework = osgiFramework; this.bundleLoader = bundleLoader; diff --git a/container-core/src/main/java/com/yahoo/container/handler/ClustersStatus.java b/container-core/src/main/java/com/yahoo/container/handler/ClustersStatus.java index aab13a1cc7b..0ed0daa2141 100644 --- a/container-core/src/main/java/com/yahoo/container/handler/ClustersStatus.java +++ b/container-core/src/main/java/com/yahoo/container/handler/ClustersStatus.java @@ -27,6 +27,8 @@ public class ClustersStatus extends AbstractComponent { @Inject public ClustersStatus() { } + public enum Require {ONE, ALL} + /** Are there any (in-service influencing) clusters in this container? */ private boolean containerHasClusters; @@ -72,12 +74,25 @@ public class ClustersStatus extends AbstractComponent { setDown((String) clusterIdentifier); } - /** Returns whether this container should receive traffic based on the state of this */ + @Deprecated // TODO: Remove on Vespa 8 public boolean containerShouldReceiveTraffic() { + return containerShouldReceiveTraffic(Require.ONE); + } + /** + * Returns whether this container should receive traffic based on the state of this + * @param require Requirement for being up, ALL or ONE. + */ + public boolean containerShouldReceiveTraffic(Require require) { synchronized (mutex) { if (containerHasClusters) { - // Should receive traffic when at least one cluster is up - return clusterStatus.values().stream().anyMatch(status -> status==true); + switch (require) { + case ONE: + // Should receive traffic when at least one cluster is up + return clusterStatus.values().stream().anyMatch(status -> status == true); + case ALL: + default: + return !clusterStatus.isEmpty() && clusterStatus.values().stream().allMatch(status -> status == true); + } } else { return true; diff --git a/container-core/src/main/java/com/yahoo/container/handler/ThreadPoolProvider.java b/container-core/src/main/java/com/yahoo/container/handler/ThreadPoolProvider.java index 4cc3b48fd1a..b427a58c9b7 100644 --- a/container-core/src/main/java/com/yahoo/container/handler/ThreadPoolProvider.java +++ b/container-core/src/main/java/com/yahoo/container/handler/ThreadPoolProvider.java @@ -9,6 +9,7 @@ import com.yahoo.container.di.componentgraph.Provider; import com.yahoo.container.protect.ProcessTerminator; import com.yahoo.jdisc.Metric; +import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; @@ -43,7 +44,8 @@ public class ThreadPoolProvider extends AbstractComponent implements Provider<Ex threadpoolConfig.maxthreads(), 0L, TimeUnit.SECONDS, new SynchronousQueue<>(false), - ThreadFactoryFactory.getThreadFactory("threadpool")); + ThreadFactoryFactory.getThreadFactory("threadpool"), + metric); // Prestart needed, if not all threads will be created by the fist N tasks and hence they might also // get the dreaded thread locals initialized even if they will never run. // That counters what we we want to achieve with the Q that will prefer thread locality. @@ -161,17 +163,22 @@ public class ThreadPoolProvider extends AbstractComponent implements Provider<Ex /** A thread pool executor which maintains the last time a worker completed */ private final static class WorkerCompletionTimingThreadPoolExecutor extends ThreadPoolExecutor { + private static final String UNHANDLED_EXCEPTIONS_METRIC = "jdisc.thread_pool.unhandled_exceptions"; + volatile long lastThreadAssignmentTimeMillis = System.currentTimeMillis(); private final AtomicLong startedCount = new AtomicLong(0); private final AtomicLong completedCount = new AtomicLong(0); + private final Metric metric; public WorkerCompletionTimingThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, - ThreadFactory threadFactory) { + ThreadFactory threadFactory, + Metric metric) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); + this.metric = metric; } @Override @@ -185,6 +192,9 @@ public class ThreadPoolProvider extends AbstractComponent implements Provider<Ex protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); completedCount.incrementAndGet(); + if (t != null) { + metric.add(UNHANDLED_EXCEPTIONS_METRIC, 1L, metric.createContext(Map.of("exception", t.getClass().getSimpleName()))); + } } @Override diff --git a/container-core/src/main/java/com/yahoo/container/handler/VipStatus.java b/container-core/src/main/java/com/yahoo/container/handler/VipStatus.java index b9ef1627ce7..0bf86e8f440 100644 --- a/container-core/src/main/java/com/yahoo/container/handler/VipStatus.java +++ b/container-core/src/main/java/com/yahoo/container/handler/VipStatus.java @@ -24,6 +24,8 @@ public class VipStatus { /** If this is non-null, its value decides whether this container is in rotation */ private Boolean rotationOverride = null; + private final boolean initiallyInRotation; + /** The current state of this */ private boolean currentlyInRotation; @@ -44,20 +46,29 @@ public class VipStatus { this(new QrSearchersConfig.Builder().build(), clustersStatus); } + /** For testing */ public VipStatus(QrSearchersConfig dispatchers, ClustersStatus clustersStatus) { - this(dispatchers, clustersStatus, new StateMonitor()); + this(dispatchers, new VipStatusConfig.Builder().build(), clustersStatus, new StateMonitor()); } @Inject - public VipStatus(QrSearchersConfig dispatchers, ClustersStatus clustersStatus, StateMonitor healthState) { + public VipStatus(QrSearchersConfig dispatchers, + VipStatusConfig vipStatusConfig, + ClustersStatus clustersStatus, + StateMonitor healthState) { this.clustersStatus = clustersStatus; this.healthState = healthState; + initiallyInRotation = vipStatusConfig.initiallyInRotation(); healthState.status(StateMonitor.Status.initializing); clustersStatus.setContainerHasClusters(! dispatchers.searchcluster().isEmpty()); updateCurrentlyInRotation(); } - /** @deprecated don't pass VipStatusConfig */ + @Deprecated // TODO: Remove on Vespa 8 + public VipStatus(QrSearchersConfig dispatchers, ClustersStatus clustersStatus, StateMonitor healthState) { + this(dispatchers, new VipStatusConfig.Builder().build(), clustersStatus, healthState); + } + @Deprecated // TODO: Remove on Vespa 8 public VipStatus(QrSearchersConfig dispatchers, VipStatusConfig ignored, ClustersStatus clustersStatus) { this(dispatchers, clustersStatus); @@ -102,10 +113,20 @@ public class VipStatus { private void updateCurrentlyInRotation() { synchronized (mutex) { - if (rotationOverride != null) + if (rotationOverride != null) { currentlyInRotation = rotationOverride; - else - currentlyInRotation = clustersStatus.containerShouldReceiveTraffic(); + } else { + if (healthState.status() == StateMonitor.Status.up) { + currentlyInRotation = clustersStatus.containerShouldReceiveTraffic(ClustersStatus.Require.ONE); + } + else if (healthState.status() == StateMonitor.Status.initializing) { + currentlyInRotation = clustersStatus.containerShouldReceiveTraffic(ClustersStatus.Require.ALL) + && initiallyInRotation; + } + else { + currentlyInRotation = clustersStatus.containerShouldReceiveTraffic(ClustersStatus.Require.ALL); + } + } // Change to/from 'up' when appropriate but don't change 'initializing' to 'down' if (currentlyInRotation) diff --git a/container-core/src/main/java/com/yahoo/container/handler/metrics/MetricsV2Handler.java b/container-core/src/main/java/com/yahoo/container/handler/metrics/MetricsV2Handler.java new file mode 100644 index 00000000000..78ea62e1b3a --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/handler/metrics/MetricsV2Handler.java @@ -0,0 +1,77 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.handler.metrics; + +import ai.vespa.util.http.VespaHttpClientBuilder; +import com.google.inject.Inject; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.restapi.Path; +import com.yahoo.yolean.Exceptions; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; + +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executor; + +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.OK; + +/** + * @author gjoranv + */ +public class MetricsV2Handler extends HttpHandlerBase { + + public static final String V2_PATH = "/metrics/v2"; + static final String VALUES_PATH = V2_PATH + "/values"; + + private static final int HTTP_CONNECT_TIMEOUT = 5000; + private static final int HTTP_SOCKET_TIMEOUT = 30000; + + private final String metricsProxyUri; + private final HttpClient httpClient = createHttpClient(); + + @Inject + public MetricsV2Handler(Executor executor, + MetricsProxyApiConfig config) { + super(executor); + metricsProxyUri = "http://localhost:" + config.metricsPort() + config.metricsApiPath(); + } + + @Override + protected Optional<HttpResponse> doHandle(URI requestUri, Path apiPath, String consumer) { + if (apiPath.matches(V2_PATH)) return Optional.of(resourceListResponse(requestUri, List.of(VALUES_PATH))); + if (apiPath.matches(VALUES_PATH)) return Optional.of(valuesResponse(consumer)); + return Optional.empty(); + } + + private JsonResponse valuesResponse(String consumer) { + try { + String uri = metricsProxyUri + consumerQuery(consumer); + String metricsJson = httpClient.execute(new HttpGet(uri), new BasicResponseHandler()); + return new JsonResponse(OK, metricsJson); + } catch (IOException e) { + log.warning("Unable to retrieve metrics from " + metricsProxyUri + ": " + Exceptions.toMessageString(e)); + return new ErrorResponse(INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + private static CloseableHttpClient createHttpClient() { + return VespaHttpClientBuilder.create() + .setUserAgent("application-metrics-retriever") + .setDefaultRequestConfig(RequestConfig.custom() + .setConnectTimeout(HTTP_CONNECT_TIMEOUT) + .setSocketTimeout(HTTP_SOCKET_TIMEOUT) + .build()) + .build(); + } + + static String consumerQuery(String consumer) { + return (consumer == null || consumer.isEmpty()) ? "" : "?consumer=" + consumer; + } +} diff --git a/container-core/src/main/java/com/yahoo/container/jdisc/ThreadedHttpRequestHandler.java b/container-core/src/main/java/com/yahoo/container/jdisc/ThreadedHttpRequestHandler.java index dddde1205ca..c58d49bf8c8 100644 --- a/container-core/src/main/java/com/yahoo/container/jdisc/ThreadedHttpRequestHandler.java +++ b/container-core/src/main/java/com/yahoo/container/jdisc/ThreadedHttpRequestHandler.java @@ -13,6 +13,7 @@ import com.yahoo.log.LogLevel; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; @@ -31,6 +32,7 @@ public abstract class ThreadedHttpRequestHandler extends ThreadedRequestHandler public static final String CONTENT_TYPE = "Content-Type"; private static final String RENDERING_ERRORS = "rendering_errors"; + private static final String UNHANDLED_EXCEPTIONS_METRIC = "jdisc.http.handler.unhandled_exceptions"; /** Logger for subclasses */ protected final Logger log; @@ -79,6 +81,7 @@ public abstract class ThreadedHttpRequestHandler extends ThreadedRequestHandler channel.setHttpResponse(httpResponse); // may or may not have already been done render(httpRequest, httpResponse, channel, jdiscRequest.creationTime(TimeUnit.MILLISECONDS)); } catch (Exception e) { + metric.add(UNHANDLED_EXCEPTIONS_METRIC, 1L, contextFor(request, Map.of("exception", e.getClass().getSimpleName()))); metric.add(RENDERING_ERRORS, 1, null); log.log(LogLevel.ERROR, "Uncaught exception handling request", e); if (channel != null) { @@ -93,7 +96,7 @@ public abstract class ThreadedHttpRequestHandler extends ThreadedRequestHandler /** Render and return whether the channel was closed */ private void render(HttpRequest request, HttpResponse httpResponse, - LazyContentChannel channel, long startTime) throws IOException { + LazyContentChannel channel, long startTime) { LoggingCompletionHandler logOnCompletion = null; ContentChannelOutputStream output = null; try { @@ -168,7 +171,7 @@ public abstract class ThreadedHttpRequestHandler extends ThreadedRequestHandler @Override public void close(CompletionHandler completionHandler) { if ( closed ) return; - try { httpRequest.getData().close(); } catch (IOException e) {}; + try { httpRequest.getData().close(); } catch (IOException e) {} if (channel == null) channel = handleResponse(); try { diff --git a/container-core/src/main/java/com/yahoo/container/jdisc/ThreadedRequestHandler.java b/container-core/src/main/java/com/yahoo/container/jdisc/ThreadedRequestHandler.java index bfcecd61fa4..99732af9d31 100644 --- a/container-core/src/main/java/com/yahoo/container/jdisc/ThreadedRequestHandler.java +++ b/container-core/src/main/java/com/yahoo/container/jdisc/ThreadedRequestHandler.java @@ -17,6 +17,7 @@ import com.yahoo.jdisc.handler.ResponseDispatch; import com.yahoo.jdisc.handler.ResponseHandler; import com.yahoo.log.LogLevel; +import java.net.URI; import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -78,7 +79,7 @@ public abstract class ThreadedRequestHandler extends AbstractRequestHandler { this.allowAsyncResponse = allowAsyncResponse; } - private Metric.Context contextFor(Request request) { + Metric.Context contextFor(Request request, Map<String, String> extraDimensions) { BindingMatch match = request.getBindingMatch(); if (match == null) return null; UriPattern matched = match.matched(); @@ -91,9 +92,17 @@ public abstract class ThreadedRequestHandler extends AbstractRequestHandler { if (endpoint != null) { dimensions.put("endpoint", endpoint); } + URI uri = request.getUri(); + dimensions.put("scheme", uri.getScheme()); + dimensions.put("port", Integer.toString(uri.getPort())); + String handlerClassName = getClass().getName(); + dimensions.put("handler-name", handlerClassName); + dimensions.putAll(extraDimensions); return this.metric.createContext(dimensions); } + private Metric.Context contextFor(Request request) { return contextFor(request, Map.of()); } + /** * Handles a request by assigning a worker thread to it. * diff --git a/container-core/src/main/java/com/yahoo/container/jdisc/state/StateMonitor.java b/container-core/src/main/java/com/yahoo/container/jdisc/state/StateMonitor.java index f690c240537..faa08402cdc 100644 --- a/container-core/src/main/java/com/yahoo/container/jdisc/state/StateMonitor.java +++ b/container-core/src/main/java/com/yahoo/container/jdisc/state/StateMonitor.java @@ -14,6 +14,7 @@ import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; /** @@ -27,7 +28,7 @@ public class StateMonitor extends AbstractComponent { private final static Logger log = Logger.getLogger(StateMonitor.class.getName()); - public enum Status {up, down, initializing}; + public enum Status {up, down, initializing} private final CopyOnWriteArrayList<StateMetricConsumer> consumers = new CopyOnWriteArrayList<>(); private final Thread thread; @@ -37,6 +38,7 @@ public class StateMonitor extends AbstractComponent { private volatile MetricSnapshot snapshot; private volatile Status status; private final TreeSet<String> valueNames = new TreeSet<>(); + private final AtomicBoolean stopped = new AtomicBoolean(false); /** For testing */ public StateMonitor() { @@ -53,10 +55,16 @@ public class StateMonitor extends AbstractComponent { } StateMonitor(HealthMonitorConfig config, Timer timer, ThreadFactory threadFactory) { + this((long)(config.snapshot_interval() * TimeUnit.SECONDS.toMillis(1)), + Status.valueOf(config.initialStatus()), + timer, threadFactory); + } + /* For Testing */ + public StateMonitor(long snapshotIntervalMS, Status status, Timer timer, ThreadFactory threadFactory) { this.timer = timer; - this.snapshotIntervalMs = (long)(config.snapshot_interval() * TimeUnit.SECONDS.toMillis(1)); + this.snapshotIntervalMs = snapshotIntervalMS; this.lastSnapshotTimeMs = timer.currentTimeMillis(); - this.status = Status.valueOf(config.initialStatus()); + this.status = status; thread = threadFactory.newThread(this::run); thread.start(); } @@ -99,13 +107,13 @@ public class StateMonitor extends AbstractComponent { private void run() { log.finest("StateMonitor started."); try { - while (!Thread.interrupted()) { - checkTime(); - Thread.sleep((lastSnapshotTimeMs + snapshotIntervalMs) - timer.currentTimeMillis()); + synchronized (stopped) { + while (!stopped.get()) { + checkTime(); + stopped.wait((lastSnapshotTimeMs + snapshotIntervalMs) - timer.currentTimeMillis()); + } } - } catch (InterruptedException e) { - - } + } catch (InterruptedException e) { } log.finest("StateMonitor stopped."); } @@ -137,12 +145,13 @@ public class StateMonitor extends AbstractComponent { @Override public void deconstruct() { - thread.interrupt(); + synchronized (stopped) { + stopped.set(true); + stopped.notifyAll(); + } try { thread.join(5000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } + } catch (InterruptedException e) { } if (thread.isAlive()) { log.warning("StateMonitor failed to terminate within 5 seconds of interrupt signal. Ignoring."); } diff --git a/container-core/src/main/java/com/yahoo/restapi/ResourceResponse.java b/container-core/src/main/java/com/yahoo/restapi/ResourceResponse.java index ff301d44798..0188136addb 100644 --- a/container-core/src/main/java/com/yahoo/restapi/ResourceResponse.java +++ b/container-core/src/main/java/com/yahoo/restapi/ResourceResponse.java @@ -1,46 +1,41 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.restapi; import com.yahoo.container.jdisc.HttpRequest; -import com.yahoo.container.jdisc.HttpResponse; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.JsonFormat; import com.yahoo.slime.Slime; -import java.io.IOException; -import java.io.OutputStream; import java.net.URI; +import java.util.List; /** * Returns a response containing an array of links to sub-resources * * @author bratseth */ -public class ResourceResponse extends HttpResponse { +public class ResourceResponse extends SlimeJsonResponse { - private final Slime slime = new Slime(); + public ResourceResponse(URI parentUrl, List<String> subResources) { + super(200, toSlime(parentUrl, subResources)); + } public ResourceResponse(URI parentUrl, String ... subResources) { - super(200); - Cursor resourceArray = slime.setObject().setArray("resources"); - for (String subResource : subResources) { - Cursor resourceEntry = resourceArray.addObject(); - resourceEntry.setString("url", new Uri(parentUrl).append(subResource) - .withTrailingSlash() - .toString()); - } + this(parentUrl, List.of(subResources)); } public ResourceResponse(HttpRequest request, String ... subResources) { this(request.getUri(), subResources); } - @Override - public void render(OutputStream stream) throws IOException { - new JsonFormat(true).encode(stream, slime); + private static Slime toSlime(URI parentUrl, List<String> subResources) { + var slime = new Slime(); + var resourceArray = slime.setObject().setArray("resources"); + for (var subResource : subResources) { + var resourceEntry = resourceArray.addObject(); + resourceEntry.setString("url", new Uri(parentUrl).append(subResource) + .withTrailingSlash() + .toString()); + } + return slime; } - @Override - public String getContentType() { return "application/json"; } - } diff --git a/container-core/src/main/resources/configdefinitions/container-http.def b/container-core/src/main/resources/configdefinitions/container-http.def index ccf559b862a..23edd402893 100644 --- a/container-core/src/main/resources/configdefinitions/container-http.def +++ b/container-core/src/main/resources/configdefinitions/container-http.def @@ -3,3 +3,6 @@ namespace=container.core ## If non-empty, handlers should emit a header containing this string as key and the local host name as value hostResponseHeaderKey string default="" + +## For debugging, number of requests to add trace and timing information too if debugging is enabled. +numQueriesToTraceOnDebugAfterConstruction int default=1000 diff --git a/container-core/src/main/resources/configdefinitions/health-monitor.def b/container-core/src/main/resources/configdefinitions/health-monitor.def index 5e70c72ae3f..4e91d85b2b8 100644 --- a/container-core/src/main/resources/configdefinitions/health-monitor.def +++ b/container-core/src/main/resources/configdefinitions/health-monitor.def @@ -6,4 +6,4 @@ namespace=container.jdisc.config snapshot_interval double default=300 # Initial status used in /state/v1/health API (value for 'code' in 'status'). See StateMonitor for valid values -initialStatus string default="up" +initialStatus string default="initializing" diff --git a/container-core/src/main/resources/configdefinitions/metrics-proxy-api.def b/container-core/src/main/resources/configdefinitions/metrics-proxy-api.def new file mode 100644 index 00000000000..3e5b973e3f3 --- /dev/null +++ b/container-core/src/main/resources/configdefinitions/metrics-proxy-api.def @@ -0,0 +1,6 @@ +# Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +namespace=container.handler.metrics + +metricsPort int +metricsApiPath string diff --git a/container-core/src/main/resources/configdefinitions/threadpool.def b/container-core/src/main/resources/configdefinitions/threadpool.def index 5b5e7e2f4a2..9bb9badd9b5 100644 --- a/container-core/src/main/resources/configdefinitions/threadpool.def +++ b/container-core/src/main/resources/configdefinitions/threadpool.def @@ -8,3 +8,8 @@ maxthreads int default=500 # get out of a bad state. This should be set a bit higher than the expected max execution # time of each request when in a state of overload, i.e about "worst case execution time*2" maxThreadExecutionTimeSeconds int default=190 + +# Length of period for soft start +# During this period number of availble threads will be gradually increased. +# Currently used to avoid feeding overload in container during cold start. +softStartSeconds double default=0 diff --git a/container-core/src/test/java/ai/vespa/cloud/SystemInfoTest.java b/container-core/src/test/java/ai/vespa/cloud/SystemInfoTest.java new file mode 100644 index 00000000000..6bc8b395e00 --- /dev/null +++ b/container-core/src/test/java/ai/vespa/cloud/SystemInfoTest.java @@ -0,0 +1,49 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.cloud; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author bratseth + */ +public class SystemInfoTest { + + @Test + public void testSystemInfo() { + Zone zone = new Zone(Environment.dev, "us-west-1"); + SystemInfo info = new SystemInfo(zone); + assertEquals(zone, info.zone()); + } + + @Test + public void testZone() { + Zone zone = Zone.from("dev.us-west-1"); + zone = Zone.from(zone.toString()); + assertEquals(Environment.dev, zone.environment()); + assertEquals("us-west-1", zone.region()); + Zone sameZone = Zone.from("dev.us-west-1"); + assertEquals(sameZone.hashCode(), zone.hashCode()); + assertEquals(sameZone, zone); + + try { + Zone.from("invalid"); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + assertEquals("A zone string must be on the form [environment].[region], but was 'invalid'", + e.getMessage()); + } + + try { + Zone.from("invalid.us-west-1"); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + assertEquals("Invalid zone 'invalid.us-west-1': No environment named 'invalid'", e.getMessage()); + } + } + +} diff --git a/container-core/src/test/java/com/yahoo/container/core/config/BundleManagerTest.java b/container-core/src/test/java/com/yahoo/container/core/config/BundleManagerTest.java new file mode 100644 index 00000000000..414e6b05128 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/core/config/BundleManagerTest.java @@ -0,0 +1,106 @@ +// 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 org.junit.Before; +import org.junit.Test; +import org.osgi.framework.Bundle; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author gjoranv + */ +public class BundleManagerTest { + + private static final FileReference BUNDLE_1_REF = new FileReference("bundle-1"); + private static final Bundle BUNDLE_1 = new TestBundle(BUNDLE_1_REF.value()); + private static final FileReference BUNDLE_2_REF = new FileReference("bundle-2"); + private static final Bundle BUNDLE_2 = new TestBundle(BUNDLE_2_REF.value()); + + private BundleManager bundleLoader; + private TestOsgi osgi; + + @Before + public void setup() { + osgi = new TestOsgi(testBundles()); + var bundleInstaller = new TestBundleInstaller(); + bundleLoader = new BundleManager(osgi); + bundleLoader.useCustomBundleInstaller(bundleInstaller); + } + + @Test + public void bundles_are_installed_and_started() { + bundleLoader.use(List.of(BUNDLE_1_REF)); + assertEquals(1, osgi.getInstalledBundles().size()); + + // The bundle is installed and started + TestBundle installedBundle = (TestBundle)osgi.getInstalledBundles().get(0); + assertEquals(BUNDLE_1.getSymbolicName(), installedBundle.getSymbolicName()); + assertTrue(installedBundle.started); + + // The file reference is active + assertEquals(1, bundleLoader.getActiveFileReferences().size()); + assertEquals(BUNDLE_1_REF, bundleLoader.getActiveFileReferences().get(0)); + } + + @Test + public void new_bundle_can_be_installed_in_reconfig() { + bundleLoader.use(List.of(BUNDLE_1_REF)); + Set<Bundle> obsoleteBundles = bundleLoader.use(List.of(BUNDLE_1_REF, BUNDLE_2_REF)); + + // No bundles are obsolete + assertTrue(obsoleteBundles.isEmpty()); + + // Both bundles are installed + assertEquals(2, osgi.getInstalledBundles().size()); + assertEquals(BUNDLE_1.getSymbolicName(), osgi.getInstalledBundles().get(0).getSymbolicName()); + assertEquals(BUNDLE_2.getSymbolicName(), osgi.getInstalledBundles().get(1).getSymbolicName()); + + // Both bundles are current + assertEquals(2, osgi.getCurrentBundles().size()); + assertEquals(BUNDLE_1.getSymbolicName(), osgi.getCurrentBundles().get(0).getSymbolicName()); + assertEquals(BUNDLE_2.getSymbolicName(), osgi.getCurrentBundles().get(1).getSymbolicName()); + + + // Both file references are active + assertEquals(2, bundleLoader.getActiveFileReferences().size()); + assertEquals(BUNDLE_1_REF, bundleLoader.getActiveFileReferences().get(0)); + assertEquals(BUNDLE_2_REF, bundleLoader.getActiveFileReferences().get(1)); + } + + @Test + public void unused_bundle_is_marked_obsolete_after_reconfig() { + bundleLoader.use(List.of(BUNDLE_1_REF)); + Set<Bundle> obsoleteBundles = bundleLoader.use(List.of(BUNDLE_2_REF)); + + // The returned set of obsolete bundles contains bundle-1 + assertEquals(1, obsoleteBundles.size()); + assertEquals(BUNDLE_1.getSymbolicName(), obsoleteBundles.iterator().next().getSymbolicName()); + + // Both bundles are installed + assertEquals(2, osgi.getInstalledBundles().size()); + assertEquals(BUNDLE_1.getSymbolicName(), osgi.getInstalledBundles().get(0).getSymbolicName()); + assertEquals(BUNDLE_2.getSymbolicName(), osgi.getInstalledBundles().get(1).getSymbolicName()); + + // Only bundle-2 is current + assertEquals(1, osgi.getCurrentBundles().size()); + assertEquals(BUNDLE_2.getSymbolicName(), osgi.getCurrentBundles().get(0).getSymbolicName()); + + // Only the bundle-2 file reference is active + assertEquals(1, bundleLoader.getActiveFileReferences().size()); + assertEquals(BUNDLE_2_REF, bundleLoader.getActiveFileReferences().get(0)); + } + + + private static Map<String, Bundle> testBundles() { + return Map.of(BUNDLE_1_REF.value(), BUNDLE_1, + BUNDLE_2_REF.value(), BUNDLE_2); + } + +} diff --git a/container-core/src/test/java/com/yahoo/container/core/config/TestBundle.java b/container-core/src/test/java/com/yahoo/container/core/config/TestBundle.java new file mode 100644 index 00000000000..421f4302c27 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/core/config/TestBundle.java @@ -0,0 +1,102 @@ +// 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.container.bundle.MockBundle; +import org.osgi.framework.Bundle; +import org.osgi.framework.Version; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; + +import java.util.List; + +/** + * @author gjoranv + */ +class TestBundle extends MockBundle { + + private static final BundleRevision revision = new TestBundleRevision(); + + private final String symbolicName; + + boolean started = false; + + TestBundle(String symbolicName) { + this.symbolicName = symbolicName; + } + + @Override + public void start() { + started = true; + } + + @Override + public String getSymbolicName() { + return symbolicName; + } + + + @SuppressWarnings("unchecked") + @Override + public <T> T adapt(Class<T> type) { + if (type.equals(BundleRevision.class)) { + return (T) revision; + } else { + throw new UnsupportedOperationException(); + } + } + + + static class TestBundleRevision implements BundleRevision { + + // Ensure this is not seen as a fragment bundle. + @Override + public int getTypes() { + return 0; + } + + @Override + public String getSymbolicName() { + throw new UnsupportedOperationException(); + } + + @Override + public Version getVersion() { + throw new UnsupportedOperationException(); + } + + @Override + public List<BundleCapability> getDeclaredCapabilities(String namespace) { + throw new UnsupportedOperationException(); + } + + @Override + public List<BundleRequirement> getDeclaredRequirements(String namespace) { + throw new UnsupportedOperationException(); + } + + @Override + public BundleWiring getWiring() { + throw new UnsupportedOperationException(); + } + + @Override + public List<Capability> getCapabilities(String namespace) { + throw new UnsupportedOperationException(); + } + + @Override + public List<Requirement> getRequirements(String namespace) { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle getBundle() { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/container-core/src/test/java/com/yahoo/container/core/config/TestBundleInstaller.java b/container-core/src/test/java/com/yahoo/container/core/config/TestBundleInstaller.java new file mode 100644 index 00000000000..43a5268eabf --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/core/config/TestBundleInstaller.java @@ -0,0 +1,20 @@ +// 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.osgi.Osgi; +import org.osgi.framework.Bundle; + +import java.util.List; + +/** + * @author gjoranv + */ +class TestBundleInstaller implements BundleInstaller { + + @Override + public List<Bundle> installBundles(FileReference reference, Osgi osgi) { + return osgi.install(reference.value()); + } + +} diff --git a/container-core/src/test/java/com/yahoo/container/core/config/TestOsgi.java b/container-core/src/test/java/com/yahoo/container/core/config/TestOsgi.java new file mode 100644 index 00000000000..54a3159239c --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/core/config/TestOsgi.java @@ -0,0 +1,57 @@ +// 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.osgi.MockOsgi; +import org.osgi.framework.Bundle; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * @author gjoranv + */ +class TestOsgi extends MockOsgi { + + private final Map<String, Bundle> availableBundles; + + private final List<Bundle> installedBundles = new ArrayList<>(); + private final List<Bundle> allowedDuplicates = new ArrayList<>(); + + TestOsgi(Map<String, Bundle> availableBundles) { + this.availableBundles = availableBundles; + } + + @Override + public List<Bundle> install(String fileReferenceValue) { + if (! availableBundles.containsKey(fileReferenceValue)) + throw new IllegalArgumentException("No such bundle: " + fileReferenceValue); + + Bundle bundle = availableBundles.get(fileReferenceValue); + installedBundles.add(bundle); + return List.of(bundle); + } + + @Override + public Bundle[] getBundles() { + return installedBundles.toArray(new Bundle[0]); + } + + public List<Bundle> getInstalledBundles() { + return installedBundles; + } + + @Override + public List<Bundle> getCurrentBundles() { + var currentBundles = new ArrayList<>(installedBundles); + currentBundles.removeAll(allowedDuplicates); + return currentBundles; + } + + @Override + public void allowDuplicateBundles(Collection<Bundle> bundles) { + allowedDuplicates.addAll(bundles); + } + +} diff --git a/container-core/src/test/java/com/yahoo/container/handler/VipStatusTestCase.java b/container-core/src/test/java/com/yahoo/container/handler/VipStatusTestCase.java index 52679c15957..e13debcddda 100644 --- a/container-core/src/test/java/com/yahoo/container/handler/VipStatusTestCase.java +++ b/container-core/src/test/java/com/yahoo/container/handler/VipStatusTestCase.java @@ -4,6 +4,9 @@ package com.yahoo.container.handler; import static org.junit.Assert.*; import com.yahoo.container.QrSearchersConfig; +import com.yahoo.container.core.VipStatusConfig; +import com.yahoo.container.jdisc.state.StateMonitor; +import com.yahoo.jdisc.core.SystemTimer; import org.junit.Test; /** @@ -13,44 +16,101 @@ import org.junit.Test; */ public class VipStatusTestCase { - @Test - public void testVipStatusWorksWithClusters() { + private static QrSearchersConfig getSearchersConfig(String[] clusters) { var b = new QrSearchersConfig.Builder(); - var searchClusterB = new QrSearchersConfig.Searchcluster.Builder(); - searchClusterB.name("cluster1"); - searchClusterB.name("cluster2"); - searchClusterB.name("cluster3"); - b.searchcluster(searchClusterB); - VipStatus v = new VipStatus(b.build()); + if (clusters.length > 0) { + var searchClusterB = new QrSearchersConfig.Searchcluster.Builder(); + for (String cluster : clusters) { + searchClusterB.name(cluster); + } + b.searchcluster(searchClusterB); + } + return b.build(); + } + + private static VipStatus getVipStatus(String[] clusters, StateMonitor.Status startState, boolean initiallyInRotation) { + return new VipStatus(getSearchersConfig(clusters), + new VipStatusConfig.Builder().initiallyInRotation(initiallyInRotation).build(), + new ClustersStatus(), + new StateMonitor(1000, startState, new SystemTimer(), runnable -> { + Thread thread = new Thread(runnable, "StateMonitor"); + thread.setDaemon(true); + return thread; + })); + } - String cluster1 = "cluster1"; - String cluster2 = "cluster2"; - String cluster3 = "cluster3"; + private static void remove(String[] clusters, VipStatus v) { + for (String s : clusters) { + v.removeFromRotation(s); + } + } + + private static void add(String[] clusters, VipStatus v) { + for (String s : clusters) { + v.addToRotation(s); + } + } + private static void verifyUpOrDown(String[] clusters, StateMonitor.Status status) { + VipStatus v = getVipStatus(clusters, status, true); + remove(clusters, v); // initial state assertFalse(v.isInRotation()); + v.addToRotation(clusters[0]); + assertFalse(v.isInRotation()); + v.addToRotation(clusters[1]); + assertFalse(v.isInRotation()); + v.addToRotation(clusters[2]); + assertTrue(v.isInRotation()); + } + + @Test + public void testInitializingOrDownRequireAllUp() { + String[] clusters = {"cluster1", "cluster2", "cluster3"}; + verifyUpOrDown(clusters, StateMonitor.Status.initializing); + verifyUpOrDown(clusters, StateMonitor.Status.down); + } + + @Test + public void testUpRequireAllDown() { + String[] clusters = {"cluster1", "cluster2", "cluster3"}; - // one cluster becomes up - v.addToRotation(cluster1); + VipStatus v = getVipStatus(clusters, StateMonitor.Status.initializing, true); + assertFalse(v.isInRotation()); + add(clusters, v); assertTrue(v.isInRotation()); - // all clusters down - v.removeFromRotation(cluster1); - v.removeFromRotation(cluster2); - v.removeFromRotation(cluster3); + v.removeFromRotation(clusters[0]); + assertTrue(v.isInRotation()); + v.removeFromRotation(clusters[1]); + assertTrue(v.isInRotation()); + v.removeFromRotation(clusters[2]); + assertFalse(v.isInRotation()); // All down + v.addToRotation(clusters[1]); assertFalse(v.isInRotation()); - // some clusters down - v.addToRotation(cluster2); + v.addToRotation(clusters[0]); + v.addToRotation(clusters[2]); + assertTrue(v.isInRotation()); // All up + v.removeFromRotation(clusters[0]); + v.removeFromRotation(clusters[2]); assertTrue(v.isInRotation()); - // all clusters up - v.addToRotation(cluster1); - v.addToRotation(cluster3); + v.addToRotation(clusters[0]); + v.addToRotation(clusters[2]); assertTrue(v.isInRotation()); - // and down again - v.removeFromRotation(cluster1); - v.removeFromRotation(cluster2); - v.removeFromRotation(cluster3); + } + + @Test + public void testNoClustersConfiguringInitiallyInRotationFalse() { + String[] clusters = {}; + VipStatus v = getVipStatus(clusters, StateMonitor.Status.initializing, false); assertFalse(v.isInRotation()); } -} + @Test + public void testNoClustersConfiguringInitiallyInRotationTrue() { + String[] clusters = {}; + VipStatus v = getVipStatus(clusters, StateMonitor.Status.initializing, true); + assertTrue(v.isInRotation()); + } + +}
\ No newline at end of file diff --git a/container-core/src/test/java/com/yahoo/container/handler/metrics/MetricsV2HandlerTest.java b/container-core/src/test/java/com/yahoo/container/handler/metrics/MetricsV2HandlerTest.java new file mode 100644 index 00000000000..b57814e50aa --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/handler/metrics/MetricsV2HandlerTest.java @@ -0,0 +1,143 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.handler.metrics; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.yahoo.container.jdisc.RequestHandlerTestDriver; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static com.yahoo.container.handler.metrics.MetricsV2Handler.V2_PATH; +import static com.yahoo.container.handler.metrics.MetricsV2Handler.VALUES_PATH; +import static com.yahoo.container.handler.metrics.MetricsV2Handler.consumerQuery; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author gjoranv + */ +public class MetricsV2HandlerTest { + + private static final String URI_BASE = "http://localhost"; + + private static final String V2_URI = URI_BASE + V2_PATH; + private static final String VALUES_URI = URI_BASE + VALUES_PATH; + + // Mock applicationmetrics api + private static final String MOCK_METRICS_PATH = "/node0"; + + private static final String TEST_FILE = "application-metrics.json"; + private static final String RESPONSE = getFileContents(TEST_FILE); + private static final String CPU_METRIC = "cpu.util"; + private static final String REPLACED_CPU_METRIC = "replaced_cpu_util"; + private static final String CUSTOM_CONSUMER = "custom-consumer"; + + private static RequestHandlerTestDriver testDriver; + + @Rule + public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort()); + + @Before + public void setup() { + setupWireMock(); + var handler = new MetricsV2Handler(Executors.newSingleThreadExecutor(), + new MetricsProxyApiConfig.Builder() + .metricsPort(wireMockRule.port()) + .metricsApiPath(MOCK_METRICS_PATH) + .build()); + testDriver = new RequestHandlerTestDriver(handler); + } + + private void setupWireMock() { + wireMockRule.stubFor(get(urlPathEqualTo(MOCK_METRICS_PATH)) + .willReturn(aResponse().withBody(RESPONSE))); + + // Add a slightly different response for a custom consumer. + String myConsumerResponse = RESPONSE.replaceAll(CPU_METRIC, REPLACED_CPU_METRIC); + wireMockRule.stubFor(get(urlPathEqualTo(MOCK_METRICS_PATH)) + .withQueryParam("consumer", equalTo(CUSTOM_CONSUMER)) + .willReturn(aResponse().withBody(myConsumerResponse))); + } + + @Test + public void v2_response_contains_values_uri() throws Exception { + String response = testDriver.sendRequest(V2_URI).readAll(); + JSONObject root = new JSONObject(response); + assertTrue(root.has("resources")); + + JSONArray resources = root.getJSONArray("resources"); + assertEquals(1, resources.length()); + + JSONObject valuesUri = resources.getJSONObject(0); + assertEquals(VALUES_URI, valuesUri.getString("url")); + } + + @Ignore + @Test + public void visually_inspect_values_response() throws Exception { + JSONObject responseJson = getResponseAsJson(null); + System.out.println(responseJson.toString(4)); + } + + @Test + public void invalid_path_yields_error_response() throws Exception { + String response = testDriver.sendRequest(V2_URI + "/invalid").readAll(); + JSONObject root = new JSONObject(response); + assertTrue(root.has("error")); + assertTrue(root.getString("error" ).startsWith("No content")); + } + + @Test + public void values_response_is_equal_to_test_file() { + String response = testDriver.sendRequest(VALUES_URI).readAll(); + assertEquals(RESPONSE, response); + } + + @Test + public void consumer_is_propagated_to_metrics_proxy_api() throws JSONException { + JSONObject responseJson = getResponseAsJson(CUSTOM_CONSUMER); + + JSONObject firstNodeMetricsValues = + responseJson.getJSONArray("nodes").getJSONObject(0) + .getJSONObject("node") + .getJSONArray("metrics").getJSONObject(0) + .getJSONObject("values"); + + assertTrue(firstNodeMetricsValues.has(REPLACED_CPU_METRIC)); + } + + private JSONObject getResponseAsJson(String consumer) { + String response = testDriver.sendRequest(VALUES_URI + consumerQuery(consumer)).readAll(); + try { + return new JSONObject(response); + } catch (JSONException e) { + fail("Failed to create json object: " + e.getMessage()); + throw new RuntimeException(e); + } + } + + private static String getFileContents(String filename) { + InputStream in = MetricsV2HandlerTest.class.getClassLoader().getResourceAsStream(filename); + if (in == null) { + throw new RuntimeException("File not found: " + filename); + } + return new BufferedReader(new InputStreamReader(in)).lines().collect(Collectors.joining("\n")); + } + +} diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/ThreadedHttpRequestHandlerTest.java b/container-core/src/test/java/com/yahoo/container/jdisc/ThreadedHttpRequestHandlerTest.java new file mode 100644 index 00000000000..07dba21e5b6 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/jdisc/ThreadedHttpRequestHandlerTest.java @@ -0,0 +1,57 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.jdisc; + +import com.yahoo.jdisc.Metric; +import org.junit.Test; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author bjorncs + */ +public class ThreadedHttpRequestHandlerTest { + + @Test + public void unhandled_exceptions_metric_is_incremented_if_subclassed_handler_throws_exception() { + MetricMock metricMock = new MetricMock(); + ThreadedHttpRequestHandlerThrowingException handler = new ThreadedHttpRequestHandlerThrowingException(metricMock); + RequestHandlerTestDriver driver = new RequestHandlerTestDriver(handler); + + driver.sendRequest("http://localhost/myhandler"); + String expectedMetricName = "jdisc.http.handler.unhandled_exceptions"; + assertThat(metricMock.addInvocations) + .containsKey(expectedMetricName); + assertThat(metricMock.addInvocations.get(expectedMetricName).dimensions) + .containsEntry("exception", "DummyException"); + } + + private static class MetricMock implements Metric { + final ConcurrentHashMap<String, SimpleMetricContext> addInvocations = new ConcurrentHashMap<>(); + + @Override public void add(String key, Number val, Context ctx) { + addInvocations.put(key, (SimpleMetricContext)ctx); + } + @Override public void set(String key, Number val, Context ctx) {} + @Override public Context createContext(Map<String, ?> properties) { return new SimpleMetricContext(properties); } + } + + private static class SimpleMetricContext implements Metric.Context { + final Map<String, String> dimensions; + + @SuppressWarnings("unchecked") + SimpleMetricContext(Map<String, ?> dimensions) { this.dimensions = (Map<String, String>)dimensions; } + } + + private static class ThreadedHttpRequestHandlerThrowingException extends ThreadedHttpRequestHandler { + ThreadedHttpRequestHandlerThrowingException(Metric metric) { + super(Executors.newSingleThreadExecutor(), metric); + } + @Override public HttpResponse handle(HttpRequest request) { throw new DummyException(); } + } + + private static class DummyException extends RuntimeException {} +}
\ No newline at end of file diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTestBase.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTestBase.java index 78541137db5..8a1640e2c0e 100644 --- a/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTestBase.java +++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTestBase.java @@ -57,7 +57,8 @@ public class StateHandlerTestBase { HealthMonitorConfig healthMonitorConfig = new HealthMonitorConfig( new HealthMonitorConfig.Builder() - .snapshot_interval(TimeUnit.MILLISECONDS.toSeconds(SNAPSHOT_INTERVAL))); + .snapshot_interval(TimeUnit.MILLISECONDS.toSeconds(SNAPSHOT_INTERVAL)) + .initialStatus("up")); ThreadFactory threadFactory = ignored -> mock(Thread.class); this.monitor = new StateMonitor(healthMonitorConfig, timer, threadFactory); builder.guiceModules().install(new AbstractModule() { diff --git a/container-core/src/test/resources/application-metrics.json b/container-core/src/test/resources/application-metrics.json new file mode 100644 index 00000000000..52cbb721bb1 --- /dev/null +++ b/container-core/src/test/resources/application-metrics.json @@ -0,0 +1,92 @@ +{ + "nodes": [ + { + "hostname": "node0", + "role": "role0", + "node": { + "timestamp": 1234, + "metrics": [ + { + "values": { + "cpu.util": 16.222 + }, + "dimensions": { + "state": "active" + } + } + ] + }, + "services": [ + { + "name": "searchnode", + "timestamp": 1234, + "status": { + "code": "up" + }, + "metrics": [ + { + "values": { + "queries.count": 4 + }, + "dimensions": { + "documentType": "music" + } + } + ] + } + ] + }, + { + "hostname": "node1", + "role": "role1", + "node": { + "timestamp": 1234, + "metrics": [ + { + "values": { + "cpu.util": 32.444 + }, + "dimensions": { + "state": "active" + } + } + ] + }, + "services": [ + { + "name": "searchnode", + "timestamp": 1234, + "status": { + "code": "up" + }, + "metrics": [ + { + "values": { + "queries.count": 8 + }, + "dimensions": { + "documentType": "music" + } + } + ] + }, + { + "name": "slobrok", + "timestamp": 1234, + "status": { + "code": "unknown", + "description": "Unable to fetch metrics from service 'slobrok'" + }, + "metrics": [ + { + "values": {}, + "dimensions": { + "instance": "slobrok0" + } + } + ] + } + ] + } + ] +} |