summaryrefslogtreecommitdiffstats
path: root/container-core
diff options
context:
space:
mode:
Diffstat (limited to 'container-core')
-rw-r--r--container-core/CMakeLists.txt1
-rw-r--r--container-core/abi-spec.json75
-rw-r--r--container-core/pom.xml20
-rw-r--r--container-core/src/main/java/ai/vespa/cloud/Environment.java13
-rw-r--r--container-core/src/main/java/ai/vespa/cloud/SystemInfo.java31
-rw-r--r--container-core/src/main/java/ai/vespa/cloud/Zone.java61
-rw-r--r--container-core/src/main/java/ai/vespa/cloud/package-info.java10
-rw-r--r--container-core/src/main/java/com/yahoo/container/Server.java45
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/BundleInstaller.java21
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/BundleManager.java (renamed from container-core/src/main/java/com/yahoo/container/core/config/BundleLoader.java)201
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/DiskBundleInstaller.java31
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/FileAcquirerBundleInstaller.java53
-rw-r--r--container-core/src/main/java/com/yahoo/container/core/config/HandlersConfigurerDi.java7
-rw-r--r--container-core/src/main/java/com/yahoo/container/handler/ClustersStatus.java21
-rw-r--r--container-core/src/main/java/com/yahoo/container/handler/ThreadPoolProvider.java14
-rw-r--r--container-core/src/main/java/com/yahoo/container/handler/VipStatus.java33
-rw-r--r--container-core/src/main/java/com/yahoo/container/handler/metrics/MetricsV2Handler.java77
-rw-r--r--container-core/src/main/java/com/yahoo/container/jdisc/ThreadedHttpRequestHandler.java7
-rw-r--r--container-core/src/main/java/com/yahoo/container/jdisc/ThreadedRequestHandler.java11
-rw-r--r--container-core/src/main/java/com/yahoo/container/jdisc/state/StateMonitor.java35
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/ResourceResponse.java39
-rw-r--r--container-core/src/main/resources/configdefinitions/container-http.def3
-rw-r--r--container-core/src/main/resources/configdefinitions/health-monitor.def2
-rw-r--r--container-core/src/main/resources/configdefinitions/metrics-proxy-api.def6
-rw-r--r--container-core/src/main/resources/configdefinitions/threadpool.def5
-rw-r--r--container-core/src/test/java/ai/vespa/cloud/SystemInfoTest.java49
-rw-r--r--container-core/src/test/java/com/yahoo/container/core/config/BundleManagerTest.java106
-rw-r--r--container-core/src/test/java/com/yahoo/container/core/config/TestBundle.java102
-rw-r--r--container-core/src/test/java/com/yahoo/container/core/config/TestBundleInstaller.java20
-rw-r--r--container-core/src/test/java/com/yahoo/container/core/config/TestOsgi.java57
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/VipStatusTestCase.java114
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/metrics/MetricsV2HandlerTest.java143
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/ThreadedHttpRequestHandlerTest.java57
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTestBase.java3
-rw-r--r--container-core/src/test/resources/application-metrics.json92
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"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}