diff options
author | Jon Bratseth <bratseth@gmail.com> | 2021-09-27 07:08:36 +0000 |
---|---|---|
committer | Jon Bratseth <bratseth@gmail.com> | 2021-09-27 07:08:36 +0000 |
commit | fe64026f259df982a7c5a5bdf093603dfb3e486d (patch) | |
tree | af2eb9124be1bcec9c359a8029ef1425eea3afdb | |
parent | 4f0e63ee85a845b4b959bd70a9661b63cb590344 (diff) | |
parent | 9377da84086392e118d69b467006e73fe9ae3f70 (diff) |
Merge branch 'master' into bratseth/linguistics-components
Conflicts:
linguistics-components/src/main/resources/configdefinitions/sentence-piece.def
linguistics/src/main/resources/configdefinitions/language.sentencepiece.sentence-piece.def
linguistics/src/main/resources/configdefinitions/sentence-piece.def
136 files changed, 1555 insertions, 1703 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bae27287f2..c9980fb1928 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,7 +58,6 @@ add_subdirectory(config) add_subdirectory(config-model-fat) add_subdirectory(configd) add_subdirectory(configdefinitions) -add_subdirectory(configgen) add_subdirectory(configserver) add_subdirectory(configserver-flags) add_subdirectory(configutil) diff --git a/application-model/src/main/java/com/yahoo/vespa/applicationmodel/ServiceCluster.java b/application-model/src/main/java/com/yahoo/vespa/applicationmodel/ServiceCluster.java index f7398d0478f..b4a0c3bd578 100644 --- a/application-model/src/main/java/com/yahoo/vespa/applicationmodel/ServiceCluster.java +++ b/application-model/src/main/java/com/yahoo/vespa/applicationmodel/ServiceCluster.java @@ -113,6 +113,14 @@ public class ServiceCluster { } public String nodeDescription(boolean plural) { + return entityDescription("node", plural); + } + + public String serviceDescription(boolean plural) { + return entityDescription("service", plural); + } + + private String entityDescription(String entity, boolean plural) { String pluralSuffix = plural ? "s" : ""; return isConfigServer() ? "config server" + pluralSuffix : isConfigServerHost() ? "config server host" + pluralSuffix : @@ -121,7 +129,7 @@ public class ServiceCluster { isProxy() ? (plural ? "proxies" : "proxy") : isProxyHost() ? "proxy host" + pluralSuffix : isTenantHost() ? "tenant host" + pluralSuffix : - "node" + pluralSuffix + " of {" + serviceType + "," + clusterId + "}"; + entity + pluralSuffix + " of {" + serviceType + "," + clusterId + "}"; } private boolean isHostedVespaApplicationWithId(ApplicationInstanceId id) { diff --git a/client/go/Makefile b/client/go/Makefile index 17748d765c8..a86feb456ef 100644 --- a/client/go/Makefile +++ b/client/go/Makefile @@ -2,7 +2,7 @@ # The version to release. Defaults to the current tag or revision. # Use env VERSION=X.Y.Z make ... to override -VERSION ?= $(shell git describe --tags 2> /dev/null | sed -E "s/^vespa-|-1$$//g") +VERSION ?= $(shell git describe --tags 2> /dev/null | sed "s/^v//") DEVEL_VERSION := $(shell echo "0.0.0-`git rev-parse --short HEAD`") ifeq ($(VERSION),) VERSION = $(DEVEL_VERSION) @@ -26,10 +26,11 @@ all: test checkfmt install # # Example: # -# $ git checkout vespa-X.Y.Z-1 -# $ make dist-github +# $ git checkout vX.Y.Z +# $ make dist-homebrew dist-homebrew: dist-version - brew bump-formula-pr --tag vespa-$(VERSION)-1 --version $(VERSION) vespa-cli +# TODO(mpolden): Remove --version=0 after next release + brew bump-formula-pr --tag v$(VERSION) --version=0 vespa-cli # Create a GitHub release draft for all platforms. Note that this only creates a # draft, which is not publicly visible until it's explicitly published. @@ -40,7 +41,7 @@ dist-homebrew: dist-version # # Example: # -# $ git checkout vespa-X.Y.Z-1 +# $ git checkout vX.Y.Z # $ make dist-github dist-github: dist gh release create v$(VERSION) --repo vespa-engine/vespa --notes-file $(CURDIR)/README.md --draft --title "Vespa CLI $(VERSION)" \ @@ -83,7 +84,7 @@ dist-sha256sums: dist-version: ifeq ($(VERSION),$(DEVEL_VERSION)) - $(error Invalid release version: $(VERSION). Try 'git checkout vespa-X.Y.Z-1' or 'env VERSION=X.Y.Z make ...') + $(error Invalid release version: $(VERSION). Try 'git checkout vX.Y.Z' or 'env VERSION=X.Y.Z make ...') endif # diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java index 9ee36831d6a..2870b88d105 100644 --- a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java @@ -87,6 +87,8 @@ public interface ModelContext { @ModelFeatureFlag(owners = {"baldersheim"}) default int maxConcurrentMergesPerNode() { throw new UnsupportedOperationException("TODO specify default value"); } @ModelFeatureFlag(owners = {"baldersheim"}) default int maxMergeQueueSize() { throw new UnsupportedOperationException("TODO specify default value"); } @ModelFeatureFlag(owners = {"baldersheim"}) default boolean dryRunOnnxOnSetup() { return true; } + @ModelFeatureFlag(owners = {"baldersheim"}) default boolean containerDumpHeapOnShutdownTimeout() { throw new UnsupportedOperationException("TODO specify default value"); } + @ModelFeatureFlag(owners = {"baldersheim"}) default double containerShutdownTimeout() { throw new UnsupportedOperationException("TODO specify default value"); } @ModelFeatureFlag(owners = {"geirst"}) default boolean enableFeedBlockInDistributor() { return true; } @ModelFeatureFlag(owners = {"bjorncs", "tokle"}) default List<String> allowedAthenzProxyIdentities() { return List.of(); } @ModelFeatureFlag(owners = {"vekterli"}) default int maxActivationInhibitedOutOfSyncGroups() { return 0; } diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java index 4bf20e75a5d..dea6b980692 100644 --- a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java +++ b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java @@ -63,6 +63,8 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea private double resourceLimitDisk = 0.8; private double resourceLimitMemory = 0.8; private double minNodeRatioPerGroup = 0.0; + private boolean containerDumpHeapOnShutdownTimeout = false; + private double containerShutdownTimeout = 50.0; @Override public ModelContext.FeatureFlags featureFlags() { return this; } @Override public boolean multitenant() { return multitenant; } @@ -106,7 +108,16 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea @Override public double minNodeRatioPerGroup() { return minNodeRatioPerGroup; } @Override public int metricsproxyNumThreads() { return 1; } @Override public boolean enforceRankProfileInheritance() { return enforceRankProfileInheritance; } - + @Override public double containerShutdownTimeout() { return containerShutdownTimeout; } + @Override public boolean containerDumpHeapOnShutdownTimeout() { return containerDumpHeapOnShutdownTimeout; } + public TestProperties containerDumpHeapOnShutdownTimeout(boolean value) { + containerDumpHeapOnShutdownTimeout = value; + return this; + } + public TestProperties containerShutdownTimeout(double value) { + containerShutdownTimeout = value; + return this; + } public TestProperties enforceRankProfileInheritance(boolean value) { enforceRankProfileInheritance = value; return this; diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java b/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java index b915453b593..cdf8d592391 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java @@ -74,6 +74,8 @@ public abstract class Container extends AbstractService implements private final boolean retired; /** The unique index of this node */ private final int index; + private final boolean dumpHeapOnShutdownTimeout; + private final double shutdownTimeoutS; private final ComponentGroup<Handler<?>> handlers = new ComponentGroup<>(this, "handler"); private final ComponentGroup<Component<?, ?>> components = new ComponentGroup<>(this, "components"); @@ -90,6 +92,8 @@ public abstract class Container extends AbstractService implements this.parent = parent; this.retired = retired; this.index = index; + dumpHeapOnShutdownTimeout = deployState.featureFlags().containerDumpHeapOnShutdownTimeout(); + shutdownTimeoutS = deployState.featureFlags().containerShutdownTimeout(); this.defaultHttpServer = new JettyHttpServer("DefaultHttpServer", containerClusterOrNull(parent), deployState.isHosted()); if (getHttp() == null) { addChild(defaultHttpServer); @@ -315,7 +319,9 @@ public abstract class Container extends AbstractService implements .slobrokId(serviceSlobrokId())) .filedistributor(filedistributorConfig()) .discriminator((clusterName != null ? clusterName + "." : "" ) + name) - .nodeIndex(index); + .nodeIndex(index) + .shutdown.dumpHeapOnTimeout(dumpHeapOnShutdownTimeout) + .timeout(shutdownTimeoutS); } /** Returns the jvm args set explicitly for this node */ diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java index 2505aa3b01e..912ca23dce2 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java @@ -7,6 +7,7 @@ import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.model.NullConfigModelRegistry; import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.EndpointCertificateSecrets; +import com.yahoo.config.model.api.ModelContext; import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.model.builder.xml.test.DomBuilderTest; import com.yahoo.config.model.deploy.DeployState; @@ -681,36 +682,47 @@ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase { @Test public void qrconfig_is_produced() throws IOException, SAXException { + QrConfig qr = getQrConfig(new TestProperties()); + String hostname = HostName.getLocalhost(); // Using the same way of getting hostname as filedistribution model + assertEquals("default.container.0", qr.discriminator()); + assertEquals(19102, qr.rpc().port()); + assertEquals("vespa/service/default/container.0", qr.rpc().slobrokId()); + assertTrue(qr.rpc().enabled()); + assertEquals("", qr.rpc().host()); + assertFalse(qr.restartOnDeploy()); + assertEquals("filedistribution/" + hostname, qr.filedistributor().configid()); + assertEquals(50.0, qr.shutdown().timeout(), 0.00000000000001); + assertFalse(qr.shutdown().dumpHeapOnTimeout()); + } + private QrConfig getQrConfig(ModelContext.Properties properties) throws IOException, SAXException { String servicesXml = "<services>" + - "<admin version='3.0'>" + - " <nodes count='2'/>" + - "</admin>" + - "<container id ='default' version='1.0'>" + - " <nodes>" + - " <node hostalias='node1' />" + - " </nodes>" + - "</container>" + - "</services>"; + " <admin version='3.0'>" + + " <nodes count='2'/>" + + " </admin>" + + " <container id ='default' version='1.0'>" + + " <nodes>" + + " <node hostalias='node1' />" + + " </nodes>" + + " </container>" + + "</services>"; ApplicationPackage applicationPackage = new MockApplicationPackage.Builder() .withServices(servicesXml) .build(); VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .applicationPackage(applicationPackage) - .properties(new TestProperties()) + .properties(properties) .build()); - String hostname = HostName.getLocalhost(); // Using the same way of getting hostname as filedistribution model + return model.getConfig(QrConfig.class, "default/container.0"); + } - QrConfig config = model.getConfig(QrConfig.class, "default/container.0"); - assertEquals("default.container.0", config.discriminator()); - assertEquals(19102, config.rpc().port()); - assertEquals("vespa/service/default/container.0", config.rpc().slobrokId()); - assertTrue(config.rpc().enabled()); - assertEquals("", config.rpc().host()); - assertFalse(config.restartOnDeploy()); - assertEquals("filedistribution/" + hostname, config.filedistributor().configid()); + @Test + public void control_container_shutdown() throws IOException, SAXException { + QrConfig qr = getQrConfig(new TestProperties().containerShutdownTimeout(133).containerDumpHeapOnShutdownTimeout(true)); + assertEquals(133.0, qr.shutdown().timeout(), 0.00000000000001); + assertTrue(qr.shutdown().dumpHeapOnTimeout()); } @Test diff --git a/config/src/main/java/com/yahoo/vespa/config/protocol/JRTServerConfigRequestV3.java b/config/src/main/java/com/yahoo/vespa/config/protocol/JRTServerConfigRequestV3.java index 446ddf0560b..e6c9bc2175b 100644 --- a/config/src/main/java/com/yahoo/vespa/config/protocol/JRTServerConfigRequestV3.java +++ b/config/src/main/java/com/yahoo/vespa/config/protocol/JRTServerConfigRequestV3.java @@ -3,7 +3,6 @@ package com.yahoo.vespa.config.protocol; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; -import com.yahoo.vespa.config.PayloadChecksums; import com.yahoo.jrt.DataValue; import com.yahoo.jrt.Request; import com.yahoo.jrt.StringValue; @@ -11,7 +10,7 @@ import com.yahoo.jrt.Value; import com.yahoo.text.Utf8Array; import com.yahoo.vespa.config.ConfigKey; import com.yahoo.vespa.config.ErrorCode; -import com.yahoo.vespa.config.util.ConfigUtils; +import com.yahoo.vespa.config.PayloadChecksums; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -26,8 +25,7 @@ import static com.yahoo.vespa.config.PayloadChecksum.Type.XXHASH64; * The V3 config protocol implemented on the server side. The V3 protocol uses 2 fields: * * * A metadata field containing json data describing config generation, md5 and compression info - * * A data field containing compressed or uncompressed json config payload. This field can be empty if the payload - * has not changed since last request, triggering an optimization at the client where the previous payload is used instead. + * * A data field containing compressed or uncompressed json config payload * * The implementation of addOkResponse is optimized for doing as little copying of payload data as possible, ensuring * that we get a lower memory footprint. @@ -74,8 +72,6 @@ public class JRTServerConfigRequestV3 implements JRTServerConfigRequest { @Override public void addOkResponse(Payload payload, long generation, boolean applyOnRestart, PayloadChecksums payloadChecksums) { this.applyOnRestart = applyOnRestart; - boolean changedConfig = !payloadChecksums.equals(getRequestConfigChecksums()); - boolean changedConfigAndNewGeneration = changedConfig && ConfigUtils.isGenerationNewer(generation, getRequestGeneration()); Payload responsePayload = payload.withCompression(getCompressionType()); ByteArrayOutputStream byteArrayOutputStream = new NoCopyByteArrayOutputStream(4096); try { @@ -93,10 +89,6 @@ public class JRTServerConfigRequestV3 implements JRTServerConfigRequest { throw new RuntimeException("Payload is null for ' " + this + ", not able to create response"); } CompressionInfo compressionInfo = responsePayload.getCompressionInfo(); - // If payload is not being sent, we must adjust compression info to avoid client confusion. - if (!changedConfigAndNewGeneration) { - compressionInfo = CompressionInfo.create(compressionInfo.getCompressionType(), 0); - } compressionInfo.serialize(jsonGenerator); jsonGenerator.writeEndObject(); @@ -106,17 +98,13 @@ public class JRTServerConfigRequestV3 implements JRTServerConfigRequest { throw new IllegalArgumentException("Could not add OK response for " + this); } request.returnValues().add(createResponseValue(byteArrayOutputStream)); - if (changedConfigAndNewGeneration) { - ByteBuffer buf = responsePayload.getData().wrap(); - if (buf.hasArray() && buf.remaining() == buf.array().length) { - request.returnValues().add(new DataValue(buf.array())); - } else { - byte [] dst = new byte[buf.remaining()]; - buf.get(dst); - request.returnValues().add(new DataValue(dst)); - } + ByteBuffer buf = responsePayload.getData().wrap(); + if (buf.hasArray() && buf.remaining() == buf.array().length) { + request.returnValues().add(new DataValue(buf.array())); } else { - request.returnValues().add(new DataValue(new byte[0])); + byte[] dst = new byte[buf.remaining()]; + buf.get(dst); + request.returnValues().add(new DataValue(dst)); } } diff --git a/configgen/CMakeLists.txt b/configgen/CMakeLists.txt deleted file mode 100644 index 107037f8008..00000000000 --- a/configgen/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -install_java_artifact(configgen) diff --git a/configgen/src/main/java/com/yahoo/config/codegen/CppClassBuilder.java b/configgen/src/main/java/com/yahoo/config/codegen/CppClassBuilder.java index c2539b53b28..cc234ea51d7 100644 --- a/configgen/src/main/java/com/yahoo/config/codegen/CppClassBuilder.java +++ b/configgen/src/main/java/com/yahoo/config/codegen/CppClassBuilder.java @@ -98,8 +98,12 @@ public class CppClassBuilder implements ClassBuilder { String newHeader = headerWriter.toString(); String newBody = bodyWriter.toString(); - File headerFile = new File(rootDir, relativePathUnderRoot + "/" + getFileName(root, "h")); - File bodyFile = new File(rootDir, relativePathUnderRoot + "/" + getFileName(root, "cpp")); + String prefix = ""; + if (relativePathUnderRoot != null) { + prefix = relativePathUnderRoot + "/"; + } + File headerFile = new File(rootDir, prefix + getFileName(root, "h")); + File bodyFile = new File(rootDir, prefix + getFileName(root, "cpp")); writeFile(headerFile, newHeader); writeFile(bodyFile, newBody); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java index 89987891c61..0198e5899da 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java @@ -189,6 +189,8 @@ public class ModelContextImpl implements ModelContext { private final int metricsproxyNumThreads; private final boolean enforceRankProfileInheritance; private final boolean newLocationBrokerLogic; + private final boolean containerDumpHeapOnShutdownTimeout; + private final double containerShutdownTimeout; public FeatureFlags(FlagSource source, ApplicationId appId) { this.defaultTermwiseLimit = flagValue(source, appId, Flags.DEFAULT_TERM_WISE_LIMIT); @@ -216,6 +218,8 @@ public class ModelContextImpl implements ModelContext { this.metricsproxyNumThreads = flagValue(source, appId, Flags.METRICSPROXY_NUM_THREADS); this.enforceRankProfileInheritance = flagValue(source, appId, Flags.ENFORCE_RANK_PROFILE_INHERITANCE); this.newLocationBrokerLogic = flagValue(source, appId, Flags.NEW_LOCATION_BROKER_LOGIC); + this.containerDumpHeapOnShutdownTimeout = flagValue(source, appId, Flags.CONTAINER_DUMP_HEAP_ON_SHUTDOWN_TIMEOUT); + this.containerShutdownTimeout = flagValue(source, appId,Flags.CONTAINER_SHUTDOWN_TIMEOUT); } @Override public double defaultTermwiseLimit() { return defaultTermwiseLimit; } @@ -245,6 +249,8 @@ public class ModelContextImpl implements ModelContext { @Override public double minNodeRatioPerGroup() { return minNodeRatioPerGroup; } @Override public int metricsproxyNumThreads() { return metricsproxyNumThreads; } @Override public boolean newLocationBrokerLogic() { return newLocationBrokerLogic; } + @Override public double containerShutdownTimeout() { return containerShutdownTimeout; } + @Override public boolean containerDumpHeapOnShutdownTimeout() { return containerDumpHeapOnShutdownTimeout; } @Override public boolean enforceRankProfileInheritance() { return enforceRankProfileInheritance; } diff --git a/container-core/src/main/resources/configdefinitions/container.qr.def b/container-core/src/main/resources/configdefinitions/container.qr.def index 9d9b84eb428..08a598bf4bf 100644 --- a/container-core/src/main/resources/configdefinitions/container.qr.def +++ b/container-core/src/main/resources/configdefinitions/container.qr.def @@ -30,3 +30,9 @@ nodeIndex int default=0 ## Force restart of container on deploy, and defer any changes until restart restartOnDeploy bool default=false restart + +## Force heapdump if process is not able to stop within shutdown.timeout +shutdown.dumpHeapOnTimeout bool default=false + +## Timeout for clean shutdown +shutdown.timeout double default=50.0 diff --git a/container-disc/pom.xml b/container-disc/pom.xml index f72348707c2..7d6a1ebb9d5 100644 --- a/container-disc/pom.xml +++ b/container-disc/pom.xml @@ -167,7 +167,6 @@ <buildLegacyVespaPlatformBundle>true</buildLegacyVespaPlatformBundle> <discPreInstallBundle> <!-- Vespa bundles --> - configgen.jar, config-bundle-jar-with-dependencies.jar, configdefinitions-jar-with-dependencies.jar, container-search-and-docproc-jar-with-dependencies.jar, @@ -179,7 +178,6 @@ vespaclient-container-plugin-jar-with-dependencies.jar, vespa-athenz-jar-with-dependencies.jar, security-utils-jar-with-dependencies.jar, - defaults-jar-with-dependencies.jar, zkfacade-jar-with-dependencies.jar, zookeeper-server-jar-with-dependencies.jar, <!-- Apache http client repackaged as bundle --> diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/ConfiguredApplication.java b/container-disc/src/main/java/com/yahoo/container/jdisc/ConfiguredApplication.java index 853224a5b91..457d4fc96a9 100644 --- a/container-disc/src/main/java/com/yahoo/container/jdisc/ConfiguredApplication.java +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/ConfiguredApplication.java @@ -1,6 +1,7 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.container.jdisc; +import com.google.common.util.concurrent.AtomicDouble; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Injector; @@ -43,7 +44,6 @@ import com.yahoo.vespa.config.ConfigKey; import com.yahoo.vespa.defaults.Defaults; import com.yahoo.yolean.Exceptions; -import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; @@ -54,6 +54,7 @@ import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -74,6 +75,8 @@ public final class ConfiguredApplication implements Application { private final String configId; private final OsgiFramework osgiFramework; private final com.yahoo.jdisc.Timer timerSingleton; + private final AtomicBoolean dumpHeapOnShutdownTimeout = new AtomicBoolean(false); + private final AtomicDouble shudownTimeoutS = new AtomicDouble(50.0); // Subscriber that is used when this is not a standalone-container. Subscribes // to config to make sure that container will be registered in slobrok (by {@link com.yahoo.jrt.slobrok.api.Register}) // if slobrok config changes (typically slobroks moving to other nodes) @@ -133,7 +136,7 @@ public final class ConfiguredApplication implements Application { @Override public void start() { qrConfig = getConfig(QrConfig.class, true); - + reconfigure(qrConfig); hackToInitializeServer(qrConfig); ContainerBuilder builder = createBuilderWithGuiceBindings(); @@ -222,6 +225,7 @@ public final class ConfiguredApplication implements Application { while (true) { subscriber.waitNextGeneration(false); QrConfig newConfig = QrConfig.class.cast(first(subscriber.config().values())); + reconfigure(qrConfig); if (qrConfig.rpc().port() != newConfig.rpc().port()) { com.yahoo.protect.Process.logAndDie( "Rpc port config has changed from " + @@ -235,6 +239,11 @@ public final class ConfiguredApplication implements Application { } } + void reconfigure(QrConfig qrConfig) { + dumpHeapOnShutdownTimeout.set(qrConfig.shutdown().dumpHeapOnTimeout()); + shudownTimeoutS.set(qrConfig.shutdown().timeout()); + } + private void initializeAndActivateContainer(ContainerBuilder builder) { addHandlerBindings(builder, Container.get().getRequestHandlerRegistry(), configurer.getComponent(ApplicationContext.class).discBindingsConfig); @@ -401,13 +410,11 @@ public final class ConfiguredApplication implements Application { private void startShutdownDeadlineExecutor() { shutdownDeadlineExecutor = new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory("Shutdown deadline timer")); shutdownDeadlineExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); - long delayMillis = 50 * 1000; + long delayMillis = (long)(shudownTimeoutS.get() * 1000.0); shutdownDeadlineExecutor.schedule(() -> { - String heapDumpName = Defaults.getDefaults().underVespaHome("var/crash/java_pid.") + ProcessHandle.current().pid() + ".hprof"; - try { + if (dumpHeapOnShutdownTimeout.get()) { + String heapDumpName = Defaults.getDefaults().underVespaHome("var/crash/java_pid.") + ProcessHandle.current().pid() + ".hprof"; com.yahoo.protect.Process.dumpHeap(heapDumpName, true); - } catch (IOException e) { - log.log(Level.WARNING, "Failed writing heap dump:", e); } com.yahoo.protect.Process.logAndDie( "Timed out waiting for application shutdown. Please check that all your request handlers " + diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java index f02ba85c9bf..42a5e2b42be 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java @@ -17,6 +17,7 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId; import java.time.Instant; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -80,6 +81,25 @@ public class ZmsClientMock implements ZmsClient { } @Override + public void createTenantResourceGroup(AthenzDomain tenantDomain, AthenzIdentity provider, String resourceGroup, + Set<RoleAction> roleActions) { + log("createTenantResourceGroup(tenantDomain='%s', resourceGroup='%s')", tenantDomain, resourceGroup); + AthenzDbMock.Domain domain = getDomainOrThrow(tenantDomain, true); + ApplicationId applicationId = new ApplicationId(resourceGroup); + if (!domain.applications.containsKey(applicationId)) { + domain.applications.put(applicationId, new AthenzDbMock.Application()); + } + } + + @Override + public Set<RoleAction> getTenantResourceGroups(AthenzDomain tenantDomain, AthenzIdentity provider, String resourceGroup) { + Set<RoleAction> result = new HashSet<>(); + getDomainOrThrow(tenantDomain, true).applications.get(resourceGroup).acl + .forEach((role, roleMembers) -> result.add(new RoleAction(role.roleName, role.roleName))); + return result; + } + + @Override public void addRoleMember(AthenzRole role, AthenzIdentity member, Optional<String> reason) { if ( ! role.roleName().equals("tenancy.vespa.hosting.admin")) throw new IllegalArgumentException("Mock only supports adding tenant admins, not " + role.roleName()); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java index 9bcb80f24ee..bbcfb4e35a5 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java @@ -139,6 +139,12 @@ public enum JobType { testCdUsCentral2 ("test-cd-us-central-2", Map.of(cd , ZoneId.from("prod" , "cd-us-central-2")), true), + productionCdUsEast1 ("production-cd-us-east-1", + Map.of(cd , ZoneId.from("prod" , "cd-us-east-1"))), + + testCdUsEast1 ("test-cd-us-east-1", + Map.of(cd , ZoneId.from("prod" , "cd-us-east-1")), true), + productionCdUsWest1 ("production-cd-us-west-1", Map.of(cd , ZoneId.from("prod" , "cd-us-west-1"))), diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java index 652f8630cb6..88d440b52b9 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java @@ -518,6 +518,8 @@ public class JobController { type, new Versions(platform.orElse(applicationPackage.deploymentSpec().majorVersion() .flatMap(controller.applications()::lastCompatibleVersion) + .or(() -> lastRun.map(run -> run.versions().targetPlatform()) + .filter(controller.readVersionStatus()::isActive)) .orElseGet(controller::readSystemVersion)), version, lastRun.map(run -> run.versions().targetPlatform()), diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java index 97e9a233f9f..0bf47f85420 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java @@ -46,7 +46,6 @@ public class DeploymentUpgrader extends ControllerMaintainer { Run last = controller().jobController().last(job).get(); Versions target = new Versions(systemVersion, last.versions().targetApplication(), Optional.of(last.versions().targetPlatform()), Optional.of(last.versions().targetApplication())); if ( ! deployment.version().isBefore(target.targetPlatform())) continue; - if ( controller().clock().instant().isBefore(last.start().plus(Duration.ofDays(1)))) continue; if ( ! isLikelyNightFor(job)) continue; log.log(Level.FINE, "Upgrading deployment of " + instance.id() + " in " + deployment.zone()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java index cd7ce8c3fa6..98fd0342ecd 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java @@ -24,8 +24,10 @@ import java.time.temporal.ChronoUnit; import java.util.HashSet; import java.util.Optional; import java.util.OptionalInt; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; /** * Updates refreshed endpoint certificates and triggers redeployment, and deletes unused certificates. @@ -60,6 +62,7 @@ public class EndpointCertificateMaintainer extends ControllerMaintainer { deployRefreshedCertificates(); updateRefreshedCertificates(); deleteUnusedCertificates(); + reportUnmanagedCertificates(); } catch (Exception e) { log.log(LogLevel.ERROR, "Exception caught while maintaining endpoint certificates", e); return 0.0; @@ -134,6 +137,16 @@ public class EndpointCertificateMaintainer extends ControllerMaintainer { }); } + private void reportUnmanagedCertificates() { + Set<String> managedRequestIds = curator.readAllEndpointCertificateMetadata().values().stream().map(EndpointCertificateMetadata::requestId).collect(Collectors.toSet()); + + for (EndpointCertificateMetadata cameoCertificateMetadata : endpointCertificateProvider.listCertificates()) { + if (!managedRequestIds.contains(cameoCertificateMetadata.requestId())) { + log.info("Certificate metadata exists with provider but is not managed by controller: " + cameoCertificateMetadata.requestId() + ", " + cameoCertificateMetadata.issuer() + ", " + cameoCertificateMetadata.requestedDnsSans()); + } + } + } + private Lock lock(ApplicationId applicationId) { return curator.lock(TenantAndApplicationId.from(applicationId)); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java index 6619b2ff5c6..61bf8feae35 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java @@ -14,8 +14,6 @@ import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.FlagSource; import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; import com.yahoo.vespa.hosted.controller.api.integration.horizon.HorizonClient; import com.yahoo.vespa.hosted.controller.api.integration.horizon.HorizonResponse; import com.yahoo.vespa.hosted.controller.api.role.Role; @@ -28,7 +26,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.EnumSet; -import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.logging.Level; @@ -41,7 +38,6 @@ import java.util.stream.Collectors; */ public class HorizonApiHandler extends LoggingRequestHandler { - private final BillingController billingController; private final SystemName systemName; private final HorizonClient client; private final BooleanFlag enabledHorizonDashboard; @@ -52,7 +48,6 @@ public class HorizonApiHandler extends LoggingRequestHandler { @Inject public HorizonApiHandler(LoggingRequestHandler.Context parentCtx, Controller controller, FlagSource flagSource) { super(parentCtx); - this.billingController = controller.serviceRegistry().billingController(); this.systemName = controller.system(); this.client = controller.serviceRegistry().horizonClient(); this.enabledHorizonDashboard = Flags.ENABLED_HORIZON_DASHBOARD.bindTo(flagSource); @@ -123,13 +118,11 @@ public class HorizonApiHandler extends LoggingRequestHandler { } private Set<TenantName> getAuthorizedTenants(Set<Role> roles) { - var horizonEnabled = roles.stream() + return roles.stream() .filter(TenantRole.class::isInstance) .map(role -> ((TenantRole) role).tenant()) .filter(tenant -> enabledHorizonDashboard.with(FetchVector.Dimension.TENANT_ID, tenant.value()).value()) - .collect(Collectors.toList()); - - return new HashSet<>(billingController.tenantsWithPlan(horizonEnabled, PlanId.from("pay-as-you-go"))); + .collect(Collectors.toSet()); } private static class JsonInputStreamResponse extends HttpResponse { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java index 044b7b76d1e..157f57b3bea 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java @@ -19,6 +19,7 @@ import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeStream; import com.yahoo.slime.SlimeUtils; import com.yahoo.text.Text; +import com.yahoo.vespa.configserver.flags.FlagsDb; import com.yahoo.vespa.flags.BooleanFlag; import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.FlagSource; @@ -69,15 +70,17 @@ public class UserApiHandler extends LoggingRequestHandler { private final UserManagement users; private final Controller controller; + private final FlagsDb flagsDb; private final BooleanFlag enable_public_signup_flow; private final IntFlag maxTrialTenants; private final BooleanFlag enabledHorizonDashboard; @Inject - public UserApiHandler(Context parentCtx, UserManagement users, Controller controller, FlagSource flagSource) { + public UserApiHandler(Context parentCtx, UserManagement users, Controller controller, FlagSource flagSource, FlagsDb flagsDb) { super(parentCtx); this.users = users; this.controller = controller; + this.flagsDb = flagsDb; this.enable_public_signup_flow = PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.bindTo(flagSource); this.maxTrialTenants = PermanentFlags.MAX_TRIAL_TENANTS.bindTo(flagSource); this.enabledHorizonDashboard = Flags.ENABLED_HORIZON_DASHBOARD.bindTo(flagSource); @@ -170,6 +173,7 @@ public class UserApiHandler extends LoggingRequestHandler { root.setBool("isPublic", controller.system().isPublic()); root.setBool("isCd", controller.system().isCd()); + // TODO (freva): Remove after users have migrated to use 'flags' root.setBool(enable_public_signup_flow.id().toString(), enable_public_signup_flow.with(FetchVector.Dimension.CONSOLE_USER_EMAIL, user.email()).value()); root.setBool("hasTrialCapacity", hasTrialCapacity()); @@ -197,6 +201,8 @@ public class UserApiHandler extends LoggingRequestHandler { operatorRoles.forEach(role -> operator.addString(role.definition().name())); } + UserFlagsSerializer.toSlime(root, flagsDb.getAllFlagData(), tenantRolesByTenantName.keySet(), !operatorRoles.isEmpty(), user.email()); + return new SlimeJsonResponse(slime); } @@ -249,7 +255,7 @@ public class UserApiHandler extends LoggingRequestHandler { }); } - private void toSlime(Cursor userObject, User user) { + private static void toSlime(Cursor userObject, User user) { if (user.name() != null) userObject.setString("name", user.name()); userObject.setString("email", user.email()); if (user.nickname() != null) userObject.setString("nickname", user.nickname()); @@ -376,7 +382,7 @@ public class UserApiHandler extends LoggingRequestHandler { return Exceptions.uncheck(() -> SlimeUtils.jsonToSlime(IOUtils.readBytes(request.getData(), 1 << 10)).get()); } - private <Type> Type require(String name, Function<Inspector, Type> mapper, Inspector object) { + private static <Type> Type require(String name, Function<Inspector, Type> mapper, Inspector object) { if ( ! object.field(name).valid()) throw new IllegalArgumentException("Missing field '" + name + "'."); return mapper.apply(object.field(name)); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java new file mode 100644 index 00000000000..44d537883f9 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java @@ -0,0 +1,86 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.restapi.user; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.lang.MutableBoolean; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.SlimeUtils; +import com.yahoo.vespa.flags.FetchVector; +import com.yahoo.vespa.flags.FlagDefinition; +import com.yahoo.vespa.flags.FlagId; +import com.yahoo.vespa.flags.Flags; +import com.yahoo.vespa.flags.RawFlag; +import com.yahoo.vespa.flags.UnboundFlag; +import com.yahoo.vespa.flags.json.Condition; +import com.yahoo.vespa.flags.json.FlagData; +import com.yahoo.vespa.flags.json.Rule; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author freva + */ +public class UserFlagsSerializer { + static void toSlime(Cursor cursor, Map<FlagId, FlagData> rawFlagData, + Set<TenantName> authorizedForTenantNames, boolean isOperator, String userEmail) { + FetchVector resolveVector = FetchVector.fromMap(Map.of(FetchVector.Dimension.CONSOLE_USER_EMAIL, userEmail)); + List<FlagData> filteredFlagData = Flags.getAllFlags().stream() + // Only include flags that have CONSOLE_USER_EMAIL dimension, this should be replaced with more explicit + // 'target' annotation if/when that is added to flag definition + .filter(fd -> fd.getDimensions().contains(FetchVector.Dimension.CONSOLE_USER_EMAIL)) + .map(FlagDefinition::getUnboundFlag) + .map(flag -> filteredFlagData(flag, Optional.ofNullable(rawFlagData.get(flag.id())), authorizedForTenantNames, isOperator, resolveVector)) + .collect(Collectors.toUnmodifiableList()); + + byte[] bytes = FlagData.serializeListToUtf8Json(filteredFlagData); + SlimeUtils.copyObject(SlimeUtils.jsonToSlime(bytes).get(), cursor); + } + + private static <T> FlagData filteredFlagData(UnboundFlag<T, ?, ?> definition, Optional<FlagData> original, + Set<TenantName> authorizedForTenantNames, boolean isOperator, FetchVector resolveVector) { + MutableBoolean encounteredEmpty = new MutableBoolean(false); + Optional<RawFlag> defaultValue = Optional.of(definition.serializer().serialize(definition.defaultValue())); + // Include the original rules from flag DB and the default value from code if there is no default rule in DB + List<Rule> rules = Stream.concat(original.stream().flatMap(fd -> fd.rules().stream()), Stream.of(new Rule(defaultValue))) + // Exclude rules that do not match the resolveVector + .filter(rule -> rule.partialMatch(resolveVector)) + // Re-create each rule with value explicitly set, either from DB or default from code and + // a filtered set of conditions + .map(rule -> new Rule(rule.getValueToApply().or(() -> defaultValue), + rule.conditions().stream() + .flatMap(condition -> filteredCondition(condition, authorizedForTenantNames, isOperator, resolveVector).stream()) + .collect(Collectors.toUnmodifiableList()))) + // We can stop as soon as we hit the first rule that has no conditions + .takeWhile(rule -> !encounteredEmpty.getAndSet(rule.conditions().isEmpty())) + .collect(Collectors.toUnmodifiableList()); + + return new FlagData(definition.id(), new FetchVector(), rules); + } + + private static Optional<Condition> filteredCondition(Condition condition, Set<TenantName> authorizedForTenantNames, + boolean isOperator, FetchVector resolveVector) { + // If the condition is one of the conditions that we resolve on the server, e.g. email, we do not need to + // propagate it back to the user + if (resolveVector.hasDimension(condition.dimension())) return Optional.empty(); + + // For the other dimensions, filter the values down to an allowed subset + switch (condition.dimension()) { + case TENANT_ID: return valueSubset(condition, tenant -> isOperator || authorizedForTenantNames.contains(TenantName.from(tenant))); + case APPLICATION_ID: return valueSubset(condition, appId -> isOperator || authorizedForTenantNames.stream().anyMatch(tenant -> appId.startsWith(tenant.value() + ":"))); + default: throw new IllegalArgumentException("Dimension " + condition.dimension() + " is not supported for user flags"); + } + } + + private static Optional<Condition> valueSubset(Condition condition, Predicate<String> predicate) { + Condition.CreateParams createParams = condition.toCreateParams(); + return Optional.of(createParams + .withValues(createParams.values().stream().filter(predicate).collect(Collectors.toUnmodifiableList())) + .createAs(condition.type())); + } +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgraderTest.java index ec33c8a7048..1c07a321953 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgraderTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgraderTest.java @@ -55,19 +55,13 @@ public class DeploymentUpgraderTest { assertEquals(start, tester.jobs().last(devApp.instanceId(), devUsEast1).get().start()); assertEquals(start, tester.jobs().last(prodApp.instanceId(), productionUsWest1).get().start()); - // 14 hours pass, but not upgraded before a day has passed since last deployment - tester.clock().advance(Duration.ofHours(14)); + // 11 hours pass, but not upgraded since it's not likely in the middle of the night + tester.clock().advance(Duration.ofHours(11)); upgrader.maintain(); assertEquals(start, tester.jobs().last(devApp.instanceId(), devUsEast1).get().start()); assertEquals(start, tester.jobs().last(prodApp.instanceId(), productionUsWest1).get().start()); - // 35 hours pass, but not upgraded since it's not likely in the middle of the night - tester.clock().advance(Duration.ofHours(21)); - upgrader.maintain(); - assertEquals(start, tester.jobs().last(devApp.instanceId(), devUsEast1).get().start()); - assertEquals(start, tester.jobs().last(prodApp.instanceId(), productionUsWest1).get().start()); - - // 38 hours pass, and the dev deployment, only, is upgraded + // 14 hours pass, and the dev deployment, only, is upgraded tester.clock().advance(Duration.ofHours(3)); upgrader.maintain(); assertEquals(tester.clock().instant().truncatedTo(MILLIS), tester.jobs().last(devApp.instanceId(), devUsEast1).get().start()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java index 10f143a8e96..1d844859c37 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java @@ -26,7 +26,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Optional; -import java.util.function.Consumer; import java.util.function.Supplier; import java.util.regex.Pattern; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java index 8e51f8210c7..b2b5b2286f7 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java @@ -4,7 +4,6 @@ import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.flags.InMemoryFlagSource; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; import com.yahoo.vespa.hosted.controller.api.role.Role; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest; @@ -33,8 +32,6 @@ public class HorizonApiTest extends ControllerContainerCloudTest { ((InMemoryFlagSource) tester.controller().flagSource()) .withBooleanFlag(Flags.ENABLED_HORIZON_DASHBOARD.id(), true); - tester.controller().serviceRegistry().billingController().setPlan(tenantName, PlanId.from("pay-as-you-go"), true); - tester.assertResponse(request("/horizon/v1/config/dashboard/topFolders") .roles(Set.of(Role.reader(tenantName))), "", 200); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java index acd481030e2..c884eae8afc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java @@ -5,6 +5,8 @@ import com.yahoo.application.container.handler.Request; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.utils.AthenzIdentities; +import com.yahoo.vespa.flags.Flags; +import com.yahoo.vespa.flags.PermanentFlags; import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.api.integration.user.User; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; @@ -25,40 +27,42 @@ public class UserApiOnPremTest extends ControllerContainerTest { @Test public void userMetadataOnPremTest() { - ContainerTester tester = new ContainerTester(container, responseFiles); - ControllerTester controller = new ControllerTester(tester); - User user = new User("dev@domail", "Joe Developer", "dev", null); + try (Flags.Replacer ignored = Flags.clearFlagsForTesting(PermanentFlags.MAX_TRIAL_TENANTS.id(), PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id())) { + ContainerTester tester = new ContainerTester(container, responseFiles); + ControllerTester controller = new ControllerTester(tester); + User user = new User("dev@domail", "Joe Developer", "dev", null); - controller.createTenant("tenant1", "domain1", 1L); - controller.createApplication("tenant1", "app1", "default"); - controller.createApplication("tenant1", "app2", "default"); - controller.createApplication("tenant1", "app2", "myinstance"); - controller.createApplication("tenant1", "app3"); + controller.createTenant("tenant1", "domain1", 1L); + controller.createApplication("tenant1", "app1", "default"); + controller.createApplication("tenant1", "app2", "default"); + controller.createApplication("tenant1", "app2", "myinstance"); + controller.createApplication("tenant1", "app3"); - controller.createTenant("tenant2", "domain2", 2L); - controller.createApplication("tenant2", "app2", "test"); + controller.createTenant("tenant2", "domain2", 2L); + controller.createApplication("tenant2", "app2", "test"); - controller.createTenant("tenant3", "domain3", 3L); - controller.createApplication("tenant3", "app1"); + controller.createTenant("tenant3", "domain3", 3L); + controller.createApplication("tenant3", "app1"); - controller.createTenant("sandbox", "domain4", 4L); - controller.createApplication("sandbox", "app1", "default"); - controller.createApplication("sandbox", "app2", "default"); - controller.createApplication("sandbox", "app2", "dev"); + controller.createTenant("sandbox", "domain4", 4L); + controller.createApplication("sandbox", "app1", "default"); + controller.createApplication("sandbox", "app2", "default"); + controller.createApplication("sandbox", "app2", "dev"); - AthenzIdentity operator = AthenzIdentities.from("vespa.alice"); - controller.athenzDb().addHostedOperator(operator); - AthenzIdentity tenantAdmin = AthenzIdentities.from("domain1.bob"); - Stream.of("domain1", "domain2", "domain4") - .map(AthenzDomain::new) - .map(controller.athenzDb()::getOrCreateDomain) - .forEach(d -> d.admin(AthenzIdentities.from("domain1.bob"))); + AthenzIdentity operator = AthenzIdentities.from("vespa.alice"); + controller.athenzDb().addHostedOperator(operator); + AthenzIdentity tenantAdmin = AthenzIdentities.from("domain1.bob"); + Stream.of("domain1", "domain2", "domain4") + .map(AthenzDomain::new) + .map(controller.athenzDb()::getOrCreateDomain) + .forEach(d -> d.admin(AthenzIdentities.from("domain1.bob"))); - tester.assertResponse(createUserRequest(user, operator), - new File("user-without-applications.json")); + tester.assertResponse(createUserRequest(user, operator), + new File("user-without-applications.json")); - tester.assertResponse(createUserRequest(user, tenantAdmin), - new File("user-with-applications-athenz.json")); + tester.assertResponse(createUserRequest(user, tenantAdmin), + new File("user-with-applications-athenz.json")); + } } private Request createUserRequest(User user, AthenzIdentity identity) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java index 03f1d75a50b..9198369a3ad 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.restapi.user; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.flags.InMemoryFlagSource; import com.yahoo.vespa.flags.PermanentFlags; import com.yahoo.vespa.hosted.controller.ControllerTester; @@ -205,65 +206,68 @@ public class UserApiTest extends ControllerContainerCloudTest { @Test public void userMetadataTest() { - ContainerTester tester = new ContainerTester(container, responseFiles); - ((InMemoryFlagSource) tester.controller().flagSource()) - .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true); - ControllerTester controller = new ControllerTester(tester); - Set<Role> operator = Set.of(Role.hostedOperator(), Role.hostedSupporter(), Role.hostedAccountant()); - User user = new User("dev@domail", "Joe Developer", "dev", null); - - tester.assertResponse(request("/user/v1/user") - .roles(operator) - .user(user), - new File("user-without-applications.json")); - - controller.createTenant("tenant1", Tenant.Type.cloud); - controller.createApplication("tenant1", "app1", "default"); - controller.createApplication("tenant1", "app2", "default"); - controller.createApplication("tenant1", "app2", "myinstance"); - controller.createApplication("tenant1", "app3"); - - controller.createTenant("tenant2", Tenant.Type.cloud); - controller.createApplication("tenant2", "app2", "test"); - - controller.createTenant("tenant3", Tenant.Type.cloud); - controller.createApplication("tenant3", "app1"); - - controller.createTenant("sandbox", Tenant.Type.cloud); - controller.createApplication("sandbox", "app1", "default"); - controller.createApplication("sandbox", "app2", "default"); - controller.createApplication("sandbox", "app2", "dev"); - - // Should still be empty because none of the roles explicitly refer to any of the applications - tester.assertResponse(request("/user/v1/user") - .roles(operator) - .user(user), - new File("user-without-applications.json")); - - // Empty applications because tenant dummy does not exist - tester.assertResponse(request("/user/v1/user") - .roles(Set.of(Role.administrator(TenantName.from("tenant1")), - Role.developer(TenantName.from("tenant2")), - Role.developer(TenantName.from("sandbox")), - Role.reader(TenantName.from("sandbox")))) - .user(user), - new File("user-with-applications-cloud.json")); + try (Flags.Replacer ignored = Flags.clearFlagsForTesting(PermanentFlags.MAX_TRIAL_TENANTS.id(), PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id())) { + ContainerTester tester = new ContainerTester(container, responseFiles); + ((InMemoryFlagSource) tester.controller().flagSource()) + .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true); + ControllerTester controller = new ControllerTester(tester); + Set<Role> operator = Set.of(Role.hostedOperator(), Role.hostedSupporter(), Role.hostedAccountant()); + User user = new User("dev@domail", "Joe Developer", "dev", null); + + tester.assertResponse(request("/user/v1/user") + .roles(operator) + .user(user), + new File("user-without-applications.json")); + + controller.createTenant("tenant1", Tenant.Type.cloud); + controller.createApplication("tenant1", "app1", "default"); + controller.createApplication("tenant1", "app2", "default"); + controller.createApplication("tenant1", "app2", "myinstance"); + controller.createApplication("tenant1", "app3"); + + controller.createTenant("tenant2", Tenant.Type.cloud); + controller.createApplication("tenant2", "app2", "test"); + + controller.createTenant("tenant3", Tenant.Type.cloud); + controller.createApplication("tenant3", "app1"); + + controller.createTenant("sandbox", Tenant.Type.cloud); + controller.createApplication("sandbox", "app1", "default"); + controller.createApplication("sandbox", "app2", "default"); + controller.createApplication("sandbox", "app2", "dev"); + + // Should still be empty because none of the roles explicitly refer to any of the applications + tester.assertResponse(request("/user/v1/user") + .roles(operator) + .user(user), + new File("user-without-applications.json")); + + // Empty applications because tenant dummy does not exist + tester.assertResponse(request("/user/v1/user") + .roles(Set.of(Role.administrator(TenantName.from("tenant1")), + Role.developer(TenantName.from("tenant2")), + Role.developer(TenantName.from("sandbox")), + Role.reader(TenantName.from("sandbox")))) + .user(user), + new File("user-with-applications-cloud.json")); + } } @Test public void maxTrialTenants() { - ContainerTester tester = new ContainerTester(container, responseFiles); - ((InMemoryFlagSource) tester.controller().flagSource()) - .withIntFlag(PermanentFlags.MAX_TRIAL_TENANTS.id(), 1) - .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true); - ControllerTester controller = new ControllerTester(tester); - Set<Role> operator = Set.of(Role.hostedOperator(), Role.hostedSupporter(), Role.hostedAccountant()); - User user = new User("dev@domail", "Joe Developer", "dev", null); - - controller.createTenant("tenant1", Tenant.Type.cloud); - - tester.assertResponse( - request("/user/v1/user").user(user), - new File("user-without-trial-capacity-cloud.json")); + try (Flags.Replacer ignored = Flags.clearFlagsForTesting(PermanentFlags.MAX_TRIAL_TENANTS.id(), PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id())) { + ContainerTester tester = new ContainerTester(container, responseFiles); + ((InMemoryFlagSource) tester.controller().flagSource()) + .withIntFlag(PermanentFlags.MAX_TRIAL_TENANTS.id(), 1) + .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true); + ControllerTester controller = new ControllerTester(tester); + User user = new User("dev@domail", "Joe Developer", "dev", null); + + controller.createTenant("tenant1", Tenant.Type.cloud); + + tester.assertResponse( + request("/user/v1/user").user(user), + new File("user-without-trial-capacity-cloud.json")); + } } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializerTest.java new file mode 100644 index 00000000000..8625628b74e --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializerTest.java @@ -0,0 +1,133 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.restapi.user; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yahoo.config.provision.TenantName; +import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; +import com.yahoo.test.json.JsonTestHelper; +import com.yahoo.vespa.flags.FetchVector; +import com.yahoo.vespa.flags.FlagId; +import com.yahoo.vespa.flags.Flags; +import com.yahoo.vespa.flags.JsonNodeRawFlag; +import com.yahoo.vespa.flags.json.Condition; +import com.yahoo.vespa.flags.json.FlagData; +import com.yahoo.vespa.flags.json.Rule; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.yahoo.vespa.flags.FetchVector.Dimension.APPLICATION_ID; +import static com.yahoo.vespa.flags.FetchVector.Dimension.CONSOLE_USER_EMAIL; +import static com.yahoo.vespa.flags.FetchVector.Dimension.TENANT_ID; + +/** + * @author freva + */ +public class UserFlagsSerializerTest { + + @Test + public void user_flag_test() throws IOException { + String email1 = "alice@domain.tld"; + String email2 = "bob@domain.tld"; + + try (Flags.Replacer ignored = Flags.clearFlagsForTesting()) { + Flags.defineStringFlag("string-id", "default value", List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod", CONSOLE_USER_EMAIL); + Flags.defineIntFlag("int-id", 123, List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod", CONSOLE_USER_EMAIL, TENANT_ID, APPLICATION_ID); + Flags.defineDoubleFlag("double-id", 3.14d, List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod"); + Flags.defineListFlag("list-id", List.of("a"), String.class, List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod", CONSOLE_USER_EMAIL); + Flags.defineJacksonFlag("jackson-id", new ExampleJacksonClass(123, "abc"), ExampleJacksonClass.class, + List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod", CONSOLE_USER_EMAIL, TENANT_ID); + + Map<FlagId, FlagData> flagData = Stream.of( + flagData("string-id", rule("\"value1\"", condition(CONSOLE_USER_EMAIL, Condition.Type.WHITELIST, email1))), + flagData("int-id", rule("456")), + flagData("list-id", + rule("[\"value1\"]", condition(CONSOLE_USER_EMAIL, Condition.Type.WHITELIST, email1), condition(APPLICATION_ID, Condition.Type.BLACKLIST, "tenant1:video:default", "tenant1:video:default", "tenant2:music:default")), + rule("[\"value2\"]", condition(CONSOLE_USER_EMAIL, Condition.Type.WHITELIST, email2)), + rule("[\"value1\",\"value3\"]", condition(APPLICATION_ID, Condition.Type.BLACKLIST, "tenant1:video:default", "tenant1:video:default", "tenant2:music:default"))), + flagData("jackson-id", rule("{\"integer\":456,\"string\":\"xyz\"}", condition(CONSOLE_USER_EMAIL, Condition.Type.WHITELIST, email1), condition(TENANT_ID, Condition.Type.WHITELIST, "tenant1", "tenant3"))) + ).collect(Collectors.toMap(FlagData::id, fd -> fd)); + + // double-id is not here as it does not have CONSOLE_USER_EMAIL dimension + assertUserFlags("{\"flags\":[" + + "{\"id\":\"int-id\",\"rules\":[{\"value\":456}]}," + // Default from DB + "{\"id\":\"jackson-id\",\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"tenant\"}],\"value\":{\"integer\":456,\"string\":\"xyz\"}},{\"value\":{\"integer\":123,\"string\":\"abc\"}}]}," + // Resolved for email + // Resolved for email, but conditions are empty since this user is not authorized for any tenants + "{\"id\":\"list-id\",\"rules\":[{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"application\"}],\"value\":[\"value1\"]},{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"application\"}],\"value\":[\"value1\",\"value3\"]},{\"value\":[\"a\"]}]}," + + "{\"id\":\"string-id\",\"rules\":[{\"value\":\"value1\"}]}]}", // resolved for email + flagData, Set.of(), false, email1); + + // Same as the first one, but user is authorized for tenant1 + assertUserFlags("{\"flags\":[" + + "{\"id\":\"int-id\",\"rules\":[{\"value\":456}]}," + // Default from DB + "{\"id\":\"jackson-id\",\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"tenant\",\"values\":[\"tenant1\"]}],\"value\":{\"integer\":456,\"string\":\"xyz\"}},{\"value\":{\"integer\":123,\"string\":\"abc\"}}]}," + // Resolved for email + // Resolved for email, but conditions have filtered out tenant2 + "{\"id\":\"list-id\",\"rules\":[{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"application\",\"values\":[\"tenant1:video:default\",\"tenant1:video:default\"]}],\"value\":[\"value1\"]},{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"application\",\"values\":[\"tenant1:video:default\",\"tenant1:video:default\"]}],\"value\":[\"value1\",\"value3\"]},{\"value\":[\"a\"]}]}," + + "{\"id\":\"string-id\",\"rules\":[{\"value\":\"value1\"}]}]}", // resolved for email + flagData, Set.of("tenant1"), false, email1); + + // As operator no conditions are filtered, but the email precondition is applied + assertUserFlags("{\"flags\":[" + + "{\"id\":\"int-id\",\"rules\":[{\"value\":456}]}," + // Default from DB + "{\"id\":\"jackson-id\",\"rules\":[{\"value\":{\"integer\":123,\"string\":\"abc\"}}]}," + // Default from code, no DB values match + // Includes last value from DB which is not conditioned on email and the default from code + "{\"id\":\"list-id\",\"rules\":[{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"application\",\"values\":[\"tenant1:video:default\",\"tenant1:video:default\",\"tenant2:music:default\"]}],\"value\":[\"value1\",\"value3\"]},{\"value\":[\"a\"]}]}," + + "{\"id\":\"string-id\",\"rules\":[{\"value\":\"default value\"}]}]}", // Default from code + flagData, Set.of(), true, "operator@domain.tld"); + } + } + + private static FlagData flagData(String id, Rule... rules) { + return new FlagData(new FlagId(id), new FetchVector(), rules); + } + + private static Rule rule(String data, Condition... conditions) { + return new Rule(Optional.ofNullable(data).map(JsonNodeRawFlag::fromJson), conditions); + } + + private static Condition condition(FetchVector.Dimension dimension, Condition.Type type, String... values) { + return new Condition.CreateParams(dimension).withValues(values).createAs(type); + } + + private static void assertUserFlags(String expected, Map<FlagId, FlagData> rawFlagData, + Set<String> authorizedForTenantNames, boolean isOperator, String userEmail) throws IOException { + Slime slime = new Slime(); + UserFlagsSerializer.toSlime(slime.setObject(), rawFlagData, authorizedForTenantNames.stream().map(TenantName::from).collect(Collectors.toSet()), isOperator, userEmail); + JsonTestHelper.assertJsonEquals(expected, + new String(SlimeUtils.toJsonBytes(slime), StandardCharsets.UTF_8)); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class ExampleJacksonClass { + @JsonProperty("integer") public final int integer; + @JsonProperty("string") public final String string; + private ExampleJacksonClass(@JsonProperty("integer") int integer, @JsonProperty("string") String string) { + this.integer = integer; + this.string = string; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExampleJacksonClass that = (ExampleJacksonClass) o; + return integer == that.integer && + Objects.equals(string, that.string); + } + + @Override + public int hashCode() { + return Objects.hash(integer, string); + } + } +}
\ No newline at end of file diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json index 5d3a38334ad..0a416600b2c 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json @@ -31,5 +31,6 @@ "reader" ] } - } + }, + "flags": [{"id":"enable-public-signup-flow","rules":[{"value":false}]}] } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json index e883993cb53..4e179ad83c5 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json @@ -29,5 +29,6 @@ ], "enabled-horizon-dashboard":false } - } + }, + "flags": [{"id":"enable-public-signup-flow","rules":[{"value":false}]}] } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json index 3bf999b490b..7eb445140e7 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json @@ -14,5 +14,6 @@ "hostedOperator", "hostedSupporter", "hostedAccountant" - ] + ], + "flags": [{"id":"enable-public-signup-flow","rules":[{"value":false}]}] } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json index 27242424579..3c1edab8cfc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json @@ -9,5 +9,6 @@ "nickname": "dev", "verified":false }, - "tenants": {} + "tenants": {}, + "flags": [{"id":"enable-public-signup-flow","rules":[{"value":false}]}] }
\ No newline at end of file diff --git a/defaults/CMakeLists.txt b/defaults/CMakeLists.txt index c42e5402688..ed0ab1e6fb0 100644 --- a/defaults/CMakeLists.txt +++ b/defaults/CMakeLists.txt @@ -7,4 +7,4 @@ vespa_define_module( src/apps/printdefault ) -install_fat_java_artifact(defaults) +# No separate java artifact is installed (part of config-bundle) diff --git a/dist/vespa.spec b/dist/vespa.spec index 0c5267f2847..1fa9fbc9796 100644 --- a/dist/vespa.spec +++ b/dist/vespa.spec @@ -79,6 +79,7 @@ BuildRequires: cmake3 BuildRequires: llvm7.0-devel BuildRequires: vespa-boost-devel >= 1.76.0-1 BuildRequires: vespa-gtest >= 1.8.1-1 +%define _use_vespa_gtest 1 BuildRequires: vespa-icu-devel >= 65.1.0-1 BuildRequires: vespa-lz4-devel >= 1.9.2-2 BuildRequires: vespa-onnxruntime-devel = 1.7.1 @@ -108,6 +109,7 @@ BuildRequires: vespa-boost-devel >= 1.76.0-1 BuildRequires: vespa-openssl-devel >= 1.1.1l-1 %define _use_vespa_openssl 1 BuildRequires: vespa-gtest >= 1.8.1-1 +%define _use_vespa_gtest 1 BuildRequires: vespa-lz4-devel >= 1.9.2-2 BuildRequires: vespa-onnxruntime-devel = 1.7.1 BuildRequires: vespa-protobuf-devel = 3.17.3 @@ -233,6 +235,7 @@ Requires: llvm7.0 Requires: vespa-telegraf >= 1.1.1-1 Requires: vespa-valgrind >= 3.17.0-1 %endif +Requires: vespa-gtest >= 1.8.1-1 %define _vespa_llvm_version 7 %define _extra_link_directory /usr/lib64/llvm7.0/lib;%{_vespa_deps_prefix}/lib64 %define _extra_include_directory /usr/include/llvm7.0;%{_vespa_deps_prefix}/include @@ -247,10 +250,12 @@ Requires: vespa-valgrind >= 3.17.0-1 %else %define _vespa_llvm_version 10 %endif +Requires: vespa-gtest >= 1.8.1-1 %define _extra_link_directory %{_vespa_deps_prefix}/lib64 %define _extra_include_directory %{_vespa_deps_prefix}/include %endif %if 0%{?fedora} +Requires: gtest %if 0%{?fc32} %define _vespa_llvm_version 10 %endif @@ -285,7 +290,7 @@ Requires: %{name}-tools = %{version}-%{release} # Ugly workaround because vespamalloc/src/vespamalloc/malloc/mmap.cpp uses the private # _dl_sym function. # Exclude automated requires for libraries in /opt/vespa-deps/lib64. -%global __requires_exclude ^lib(c\\.so\\.6\\(GLIBC_PRIVATE\\)|pthread\\.so\\.0\\(GLIBC_PRIVATE\\)|(icui18n|icuuc|lz4|protobuf|zstd|onnxruntime%{?_use_vespa_openssl:|crypto|ssl}%{?_use_vespa_openblas:|openblas}%{?_use_vespa_re2:|re2}%{?_use_vespa_xxhash:|xxhash})\\.so\\.[0-9.]*\\([A-Za-z._0-9]*\\))\\(64bit\\)$ +%global __requires_exclude ^lib(c\\.so\\.6\\(GLIBC_PRIVATE\\)|pthread\\.so\\.0\\(GLIBC_PRIVATE\\)|(icui18n|icuuc|lz4|protobuf|zstd|onnxruntime%{?_use_vespa_openssl:|crypto|ssl}%{?_use_vespa_openblas:|openblas}%{?_use_vespa_re2:|re2}%{?_use_vespa_xxhash:|xxhash}%{?_use_vespa_gtest:|(gtest|gmock)(_main)?})\\.so\\.[0-9.]*\\([A-Za-z._0-9]*\\))\\(64bit\\)$ %description @@ -791,7 +796,6 @@ fi %{_prefix}/lib/jars/bcprov-jdk15on-*.jar %{_prefix}/lib/jars/config-bundle-jar-with-dependencies.jar %{_prefix}/lib/jars/configdefinitions-jar-with-dependencies.jar -%{_prefix}/lib/jars/configgen.jar %{_prefix}/lib/jars/config-model-api-jar-with-dependencies.jar %{_prefix}/lib/jars/config-model-jar-with-dependencies.jar %{_prefix}/lib/jars/config-provisioning-jar-with-dependencies.jar @@ -799,7 +803,6 @@ fi %{_prefix}/lib/jars/container-disc-jar-with-dependencies.jar %{_prefix}/lib/jars/container-search-and-docproc-jar-with-dependencies.jar %{_prefix}/lib/jars/container-search-gui-jar-with-dependencies.jar -%{_prefix}/lib/jars/defaults-jar-with-dependencies.jar %{_prefix}/lib/jars/docprocs-jar-with-dependencies.jar %{_prefix}/lib/jars/flags-jar-with-dependencies.jar %{_prefix}/lib/jars/hk2-*.jar diff --git a/eval/src/apps/tensor_conformance/generate.cpp b/eval/src/apps/tensor_conformance/generate.cpp index 8a596ad38d4..d0767b45224 100644 --- a/eval/src/apps/tensor_conformance/generate.cpp +++ b/eval/src/apps/tensor_conformance/generate.cpp @@ -14,6 +14,19 @@ using vespalib::make_string_short::fmt; namespace { +struct IgnoreJava : TestBuilder { + TestBuilder &dst; + IgnoreJava(TestBuilder &dst_in) : TestBuilder(dst_in.full), dst(dst_in) {} + void add(const vespalib::string &expression, + const std::map<vespalib::string,TensorSpec> &inputs, + const std::set<vespalib::string> &ignore) override + { + auto my_ignore = ignore; + my_ignore.insert("vespajlib"); + dst.add(expression, inputs, my_ignore); + } +}; + //----------------------------------------------------------------------------- const std::vector<vespalib::string> basic_layouts = { @@ -273,6 +286,9 @@ void generate_join(TestBuilder &dst) { generate_op2_join("min(a,b)", Div16(N()), dst); generate_op2_join("max(a,b)", Div16(N()), dst); generate_op2_join("bit(a,b)", Seq({-128, -43, -1, 0, 85, 127}), Seq({0, 1, 2, 3, 4, 5, 6, 7}), dst); + // TODO: add ignored Java test when it can be ignored + // IgnoreJava ignore_java(dst); + // generate_op2_join("hamming(a,b)", Seq({-128, -43, -1, 0, 85, 127}), ignore_java); // TODO: require java // inverted lambda generate_join_expr("join(a,b,f(a,b)(b-a))", Div16(N()), dst); // custom lambda @@ -331,6 +347,9 @@ void generate_merge(TestBuilder &dst) { generate_op2_merge("min(a,b)", Div16(N()), dst); generate_op2_merge("max(a,b)", Div16(N()), dst); generate_op2_merge("bit(a,b)", Seq({-128, -43, -1, 0, 85, 127}), Seq({0, 1, 2, 3, 4, 5, 6, 7}), dst); + // TODO: add ignored Java test when it can be ignored + // IgnoreJava ignore_java(dst); + // generate_op2_merge("hamming(a,b)", Seq({-128, -43, -1, 0, 85, 127}), ignore_java); // TODO: require java // inverted lambda generate_merge_expr("merge(a,b,f(a,b)(b-a))", Div16(N()), dst); // custom lambda diff --git a/eval/src/apps/tensor_conformance/generate.h b/eval/src/apps/tensor_conformance/generate.h index e9482b9015c..9aa90ae9a7a 100644 --- a/eval/src/apps/tensor_conformance/generate.h +++ b/eval/src/apps/tensor_conformance/generate.h @@ -18,11 +18,6 @@ struct TestBuilder { { add(expression, inputs, {}); } - void add_ignore_java(const vespalib::string &expression, - const std::map<vespalib::string,TensorSpec> &inputs) - { - add(expression, inputs, {"vespajlib"}); - } virtual ~TestBuilder() {} }; diff --git a/eval/src/apps/tensor_conformance/tensor_conformance.cpp b/eval/src/apps/tensor_conformance/tensor_conformance.cpp index e6bbb1f8a41..6c28b1e652e 100644 --- a/eval/src/apps/tensor_conformance/tensor_conformance.cpp +++ b/eval/src/apps/tensor_conformance/tensor_conformance.cpp @@ -167,6 +167,15 @@ void print_test(const Inspector &test, OutputWriter &dst) { } auto result = eval_expr(test, prod_factory); dst.printf("result: %s\n", result.to_string().c_str()); + auto ignore = extract_fields(test["ignore"]); + if (!ignore.empty()) { + dst.printf("ignore:"); + for (const auto &impl: ignore) { + REQUIRE(test["ignore"][impl].asBool()); + dst.printf(" %s", impl.c_str()); + } + dst.printf("\n"); + } } //----------------------------------------------------------------------------- diff --git a/eval/src/tests/eval/inline_operation/inline_operation_test.cpp b/eval/src/tests/eval/inline_operation/inline_operation_test.cpp index ae5f503b680..8e765708574 100644 --- a/eval/src/tests/eval/inline_operation/inline_operation_test.cpp +++ b/eval/src/tests/eval/inline_operation/inline_operation_test.cpp @@ -116,6 +116,7 @@ TEST(InlineOperationTest, op2_lambdas_are_recognized) { EXPECT_EQ(as_op2("min(a,b)"), &Min::f); EXPECT_EQ(as_op2("max(a,b)"), &Max::f); EXPECT_EQ(as_op2("bit(a,b)"), &Bit::f); + EXPECT_EQ(as_op2("hamming(a,b)"), &Hamming::f); } TEST(InlineOperationTest, op2_lambdas_are_recognized_with_different_parameter_names) { diff --git a/eval/src/tests/eval/node_tools/node_tools_test.cpp b/eval/src/tests/eval/node_tools/node_tools_test.cpp index e8296c01d73..b95ea2d4b14 100644 --- a/eval/src/tests/eval/node_tools/node_tools_test.cpp +++ b/eval/src/tests/eval/node_tools/node_tools_test.cpp @@ -101,6 +101,7 @@ TEST("require that call node types can be copied") { TEST_DO(verify_copy("elu(a)")); TEST_DO(verify_copy("erf(a)")); TEST_DO(verify_copy("bit(a,b)")); + TEST_DO(verify_copy("hamming(a,b)")); } TEST("require that tensor node types can NOT be copied (yet)") { diff --git a/eval/src/tests/eval/node_types/node_types_test.cpp b/eval/src/tests/eval/node_types/node_types_test.cpp index b2373f0d8f5..5b860f0e1b3 100644 --- a/eval/src/tests/eval/node_types/node_types_test.cpp +++ b/eval/src/tests/eval/node_types/node_types_test.cpp @@ -219,6 +219,7 @@ TEST("require that various operations resolve appropriate type") { TEST_DO(verify_op1("elu(%s)")); // Elu TEST_DO(verify_op1("erf(%s)")); // Erf TEST_DO(verify_op2("bit(%s,%s)")); // Bit + TEST_DO(verify_op2("hamming(%s,%s)")); // Hamming } TEST("require that map resolves correct type") { diff --git a/eval/src/vespa/eval/eval/call_nodes.cpp b/eval/src/vespa/eval/eval/call_nodes.cpp index 798583cf89a..95dbecdd153 100644 --- a/eval/src/vespa/eval/eval/call_nodes.cpp +++ b/eval/src/vespa/eval/eval/call_nodes.cpp @@ -44,6 +44,7 @@ CallRepo::CallRepo() : _map() { add(nodes::Elu()); add(nodes::Erf()); add(nodes::Bit()); + add(nodes::Hamming()); } } // namespace vespalib::eval::nodes diff --git a/eval/src/vespa/eval/eval/call_nodes.h b/eval/src/vespa/eval/eval/call_nodes.h index 945aba69596..47fc5d6eccd 100644 --- a/eval/src/vespa/eval/eval/call_nodes.h +++ b/eval/src/vespa/eval/eval/call_nodes.h @@ -140,6 +140,7 @@ struct Sigmoid : CallHelper<Sigmoid> { Sigmoid() : Helper("sigmoid", 1) {} }; struct Elu : CallHelper<Elu> { Elu() : Helper("elu", 1) {} }; struct Erf : CallHelper<Erf> { Erf() : Helper("erf", 1) {} }; struct Bit : CallHelper<Bit> { Bit() : Helper("bit", 2) {} }; +struct Hamming : CallHelper<Hamming> { Hamming() : Helper("hamming", 2) {} }; //----------------------------------------------------------------------------- diff --git a/eval/src/vespa/eval/eval/hamming_distance.h b/eval/src/vespa/eval/eval/hamming_distance.h new file mode 100644 index 00000000000..3419de3569f --- /dev/null +++ b/eval/src/vespa/eval/eval/hamming_distance.h @@ -0,0 +1,13 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +namespace vespalib::eval { + +inline double hamming_distance(double a, double b) { + uint8_t x = (uint8_t) a; + uint8_t y = (uint8_t) b; + return __builtin_popcount(x ^ y); +} + +} diff --git a/eval/src/vespa/eval/eval/key_gen.cpp b/eval/src/vespa/eval/eval/key_gen.cpp index a40a8887119..cbbce61402c 100644 --- a/eval/src/vespa/eval/eval/key_gen.cpp +++ b/eval/src/vespa/eval/eval/key_gen.cpp @@ -88,6 +88,7 @@ struct KeyGen : public NodeVisitor, public NodeTraverser { void visit(const Elu &) override { add_byte(61); } void visit(const Erf &) override { add_byte(62); } void visit(const Bit &) override { add_byte(63); } + void visit(const Hamming &) override { add_byte(64); } // traverse bool open(const Node &node) override { node.accept(*this); return true; } diff --git a/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp b/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp index 3e4f4fe8257..a101745dca0 100644 --- a/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp +++ b/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp @@ -5,6 +5,7 @@ #include <vespa/eval/eval/node_visitor.h> #include <vespa/eval/eval/node_traverser.h> #include <vespa/eval/eval/extract_bit.h> +#include <vespa/eval/eval/hamming_distance.h> #include <llvm/IR/Verifier.h> #include <llvm/Support/TargetSelect.h> #include <llvm/IR/IRBuilder.h> @@ -31,6 +32,7 @@ double vespalib_eval_relu(double a) { return std::max(a, 0.0); } double vespalib_eval_sigmoid(double a) { return 1.0 / (1.0 + std::exp(-1.0 * a)); } double vespalib_eval_elu(double a) { return (a < 0) ? std::exp(a) - 1.0 : a; } double vespalib_eval_bit(double a, double b) { return vespalib::eval::extract_bit(a, b); } +double vespalib_eval_hamming(double a, double b) { return vespalib::eval::hamming_distance(a, b); } using vespalib::eval::gbdt::Forest; using resolve_function = double (*)(void *ctx, size_t idx); @@ -651,6 +653,9 @@ struct FunctionBuilder : public NodeVisitor, public NodeTraverser { void visit(const Bit &) override { make_call_2("vespalib_eval_bit"); } + void visit(const Hamming &) override { + make_call_2("vespalib_eval_hamming"); + } }; FunctionBuilder::~FunctionBuilder() { } diff --git a/eval/src/vespa/eval/eval/llvm/llvm_wrapper.h b/eval/src/vespa/eval/eval/llvm/llvm_wrapper.h index e04b477750d..727954d59e9 100644 --- a/eval/src/vespa/eval/eval/llvm/llvm_wrapper.h +++ b/eval/src/vespa/eval/eval/llvm/llvm_wrapper.h @@ -20,6 +20,7 @@ extern "C" { double vespalib_eval_sigmoid(double a); double vespalib_eval_elu(double a); double vespalib_eval_bit(double a, double b); + double vespalib_eval_hamming(double a, double b); }; namespace vespalib::eval { diff --git a/eval/src/vespa/eval/eval/make_tensor_function.cpp b/eval/src/vespa/eval/eval/make_tensor_function.cpp index 498be2a738b..7746676f86b 100644 --- a/eval/src/vespa/eval/eval/make_tensor_function.cpp +++ b/eval/src/vespa/eval/eval/make_tensor_function.cpp @@ -360,6 +360,9 @@ struct TensorFunctionBuilder : public NodeVisitor, public NodeTraverser { void visit(const Bit &node) override { make_join(node, operation::Bit::f); } + void visit(const Hamming &node) override { + make_join(node, operation::Hamming::f); + } //------------------------------------------------------------------------- diff --git a/eval/src/vespa/eval/eval/node_tools.cpp b/eval/src/vespa/eval/eval/node_tools.cpp index fa2d16a2271..48ee1a90b67 100644 --- a/eval/src/vespa/eval/eval/node_tools.cpp +++ b/eval/src/vespa/eval/eval/node_tools.cpp @@ -183,6 +183,7 @@ struct CopyNode : NodeTraverser, NodeVisitor { void visit(const Elu &node) override { copy_call(node); } void visit(const Erf &node) override { copy_call(node); } void visit(const Bit &node) override { copy_call(node); } + void visit(const Hamming &node) override { copy_call(node); } // traverse nodes bool open(const Node &) override { return !error; } diff --git a/eval/src/vespa/eval/eval/node_types.cpp b/eval/src/vespa/eval/eval/node_types.cpp index 8622fd734f1..2cb6e637201 100644 --- a/eval/src/vespa/eval/eval/node_types.cpp +++ b/eval/src/vespa/eval/eval/node_types.cpp @@ -279,6 +279,7 @@ struct TypeResolver : public NodeVisitor, public NodeTraverser { void visit(const Elu &node) override { resolve_op1(node); } void visit(const Erf &node) override { resolve_op1(node); } void visit(const Bit &node) override { resolve_op2(node); } + void visit(const Hamming &node) override { resolve_op2(node); } //------------------------------------------------------------------------- diff --git a/eval/src/vespa/eval/eval/node_visitor.h b/eval/src/vespa/eval/eval/node_visitor.h index 475bbf5405c..b581a94f7ee 100644 --- a/eval/src/vespa/eval/eval/node_visitor.h +++ b/eval/src/vespa/eval/eval/node_visitor.h @@ -86,6 +86,7 @@ struct NodeVisitor { virtual void visit(const nodes::Elu &) = 0; virtual void visit(const nodes::Erf &) = 0; virtual void visit(const nodes::Bit &) = 0; + virtual void visit(const nodes::Hamming &) = 0; virtual ~NodeVisitor() {} }; @@ -156,6 +157,7 @@ struct EmptyNodeVisitor : NodeVisitor { void visit(const nodes::Elu &) override {} void visit(const nodes::Erf &) override {} void visit(const nodes::Bit &) override {} + void visit(const nodes::Hamming &) override {} }; } // namespace vespalib::eval diff --git a/eval/src/vespa/eval/eval/operation.cpp b/eval/src/vespa/eval/eval/operation.cpp index a82a79e6bc4..ddd188d250f 100644 --- a/eval/src/vespa/eval/eval/operation.cpp +++ b/eval/src/vespa/eval/eval/operation.cpp @@ -4,6 +4,7 @@ #include "function.h" #include "key_gen.h" #include "extract_bit.h" +#include "hamming_distance.h" #include <vespa/vespalib/util/approx.h> #include <algorithm> @@ -52,6 +53,7 @@ double Sigmoid::f(double a) { return 1.0 / (1.0 + std::exp(-1.0 * a)); } double Elu::f(double a) { return (a < 0) ? std::exp(a) - 1 : a; } double Erf::f(double a) { return std::erf(a); } double Bit::f(double a, double b) { return extract_bit(a, b); } +double Hamming::f(double a, double b) { return hamming_distance(a, b); } //----------------------------------------------------------------------------- double Inv::f(double a) { return (1.0 / a); } double Square::f(double a) { return (a * a); } @@ -146,6 +148,7 @@ std::map<vespalib::string,op2_t> make_op2_map() { add_op2(map, "min(a,b)", Min::f); add_op2(map, "max(a,b)", Max::f); add_op2(map, "bit(a,b)", Bit::f); + add_op2(map, "hamming(a,b)", Hamming::f); return map; } diff --git a/eval/src/vespa/eval/eval/operation.h b/eval/src/vespa/eval/eval/operation.h index 438b510b714..e2a524f318c 100644 --- a/eval/src/vespa/eval/eval/operation.h +++ b/eval/src/vespa/eval/eval/operation.h @@ -50,6 +50,7 @@ struct Sigmoid { static double f(double a); }; struct Elu { static double f(double a); }; struct Erf { static double f(double a); }; struct Bit { static double f(double a, double b); }; +struct Hamming { static double f(double a, double b); }; //----------------------------------------------------------------------------- struct Inv { static double f(double a); }; struct Square { static double f(double a); }; diff --git a/eval/src/vespa/eval/eval/test/eval_spec.cpp b/eval/src/vespa/eval/eval/test/eval_spec.cpp index 5d51a1d23b5..03b3af84fc9 100644 --- a/eval/src/vespa/eval/eval/test/eval_spec.cpp +++ b/eval/src/vespa/eval/eval/test/eval_spec.cpp @@ -8,6 +8,24 @@ namespace vespalib::eval::test { +namespace { + +double byte(const vespalib::string &bits) { + int8_t res = 0; + assert(bits.size() == 8); + for (const auto &c: bits) { + if (c == '1') { + res = (res << 1) | 1; + } else { + assert(c == '0'); + res = (res << 1); + } + } + return res; +} + +} // <unnamed> + constexpr double my_nan = std::numeric_limits<double>::quiet_NaN(); constexpr double my_inf = std::numeric_limits<double>::infinity(); @@ -169,6 +187,9 @@ EvalSpec::add_function_call_cases() { .add_case({85, 3}, 0.0).add_case({85, 2}, 1.0).add_case({85, 1}, 0.0).add_case({85, 0}, 1.0) .add_case({127, 7}, 0.0).add_case({127, 6}, 1.0).add_case({127, 5}, 1.0).add_case({127, 4}, 1.0) .add_case({127, 3}, 1.0).add_case({127, 2}, 1.0).add_case({127, 1}, 1.0).add_case({127, 0}, 1.0); + add_expression({"a", "b"}, "hamming(a,b)") + .add_case({0, 0}, 0.0).add_case({-1, -1}, 0.0).add_case({-1, 0}, 8.0).add_case({0, -1}, 8.0) + .add_case({byte("11001100"), byte("10101010")}, 4.0).add_case({byte("11001100"), byte("11110000")}, 4.0); } void diff --git a/eval/src/vespa/eval/eval/test/reference_evaluation.cpp b/eval/src/vespa/eval/eval/test/reference_evaluation.cpp index 58e4b91f6d9..def3f64c1a1 100644 --- a/eval/src/vespa/eval/eval/test/reference_evaluation.cpp +++ b/eval/src/vespa/eval/eval/test/reference_evaluation.cpp @@ -338,6 +338,9 @@ struct EvalNode : public NodeVisitor { void visit(const Bit &node) override { eval_join(node.get_child(0), node.get_child(1), operation::Bit::f); } + void visit(const Hamming &node) override { + eval_join(node.get_child(0), node.get_child(1), operation::Hamming::f); + } }; TensorSpec eval_node(const Node &node, const std::vector<TensorSpec> ¶ms) { diff --git a/eval/src/vespa/eval/eval/visit_stuff.cpp b/eval/src/vespa/eval/eval/visit_stuff.cpp index 786562d823f..1d684e1c340 100644 --- a/eval/src/vespa/eval/eval/visit_stuff.cpp +++ b/eval/src/vespa/eval/eval/visit_stuff.cpp @@ -60,6 +60,7 @@ vespalib::string name_of(join_fun_t fun) { if (fun == operation::Min::f) return "min"; if (fun == operation::Max::f) return "max"; if (fun == operation::Bit::f) return "bit"; + if (fun == operation::Hamming::f) return "hamming"; return "[other join function]"; } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java index ede7bd6a109..5b3b2a94beb 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java @@ -4,9 +4,7 @@ package com.yahoo.vespa.flags; import com.yahoo.vespa.flags.json.DimensionHelper; import javax.annotation.concurrent.Immutable; -import java.util.Collections; import java.util.EnumMap; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -72,15 +70,15 @@ public class FetchVector { private final Map<Dimension, String> map; public FetchVector() { - this.map = Collections.emptyMap(); + this.map = Map.of(); } public static FetchVector fromMap(Map<Dimension, String> map) { - return new FetchVector(new HashMap<>(map)); + return new FetchVector(map); } private FetchVector(Map<Dimension, String> map) { - this.map = Collections.unmodifiableMap(map); + this.map = Map.copyOf(map); } public Optional<String> getValue(Dimension dimension) { @@ -93,6 +91,10 @@ public class FetchVector { public boolean isEmpty() { return map.isEmpty(); } + public boolean hasDimension(FetchVector.Dimension dimension) { + return map.containsKey(dimension); + } + /** * Returns a new FetchVector, identical to {@code this} except for its value in {@code dimension}. * Dimension is removed if the value is null. diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java index d01ca64cb9f..7ddbd85a904 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java @@ -3,9 +3,9 @@ package com.yahoo.vespa.flags; import javax.annotation.concurrent.Immutable; import java.time.Instant; -import java.util.Arrays; -import java.util.Collections; +import java.util.EnumSet; import java.util.List; +import java.util.Set; /** * @author hakonhall @@ -28,14 +28,14 @@ public class FlagDefinition { String description, String modificationEffect, FetchVector.Dimension... dimensions) { - validate(owners, createdAt, expiresAt); this.unboundFlag = unboundFlag; this.owners = owners; this.createdAt = createdAt; this.expiresAt = expiresAt; this.description = description; this.modificationEffect = modificationEffect; - this.dimensions = Collections.unmodifiableList(Arrays.asList(dimensions)); + this.dimensions = List.of(dimensions); + validate(owners, createdAt, expiresAt, this.dimensions); } public UnboundFlag<?, ?, ?> getUnboundFlag() { @@ -60,13 +60,14 @@ public class FlagDefinition { public Instant getExpiresAt() { return expiresAt; } - private static void validate(List<String> owners, Instant createdAt, Instant expiresAt) { + private static void validate(List<String> owners, Instant createdAt, Instant expiresAt, List<FetchVector.Dimension> dimensions) { if (expiresAt.isBefore(createdAt)) { throw new IllegalArgumentException( String.format( "Flag cannot expire before its creation date (createdAt='%s', expiresAt='%s')", createdAt, expiresAt)); } + if (owners == PermanentFlags.OWNERS) { if (!createdAt.equals(PermanentFlags.CREATED_AT) || !expiresAt.equals(PermanentFlags.EXPIRES_AT)) { throw new IllegalArgumentException("Invalid creation or expiration date for permanent flag"); @@ -74,5 +75,15 @@ public class FlagDefinition { } else if (owners.isEmpty()) { throw new IllegalArgumentException("Owner(s) must be specified"); } + + if (dimensions.contains(FetchVector.Dimension.CONSOLE_USER_EMAIL)) { + Set<FetchVector.Dimension> disallowedCombinations = EnumSet.allOf(FetchVector.Dimension.class); + disallowedCombinations.remove(FetchVector.Dimension.CONSOLE_USER_EMAIL); + disallowedCombinations.remove(FetchVector.Dimension.APPLICATION_ID); + disallowedCombinations.remove(FetchVector.Dimension.TENANT_ID); + disallowedCombinations.retainAll(dimensions); + if (!disallowedCombinations.isEmpty()) + throw new IllegalArgumentException("Dimension " + FetchVector.Dimension.CONSOLE_USER_EMAIL + " cannot be combined with " + disallowedCombinations); + } } } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index 015e063fe11..69d325d4841 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -13,6 +13,7 @@ import java.util.Optional; import java.util.TreeMap; import static com.yahoo.vespa.flags.FetchVector.Dimension.APPLICATION_ID; +import static com.yahoo.vespa.flags.FetchVector.Dimension.CONSOLE_USER_EMAIL; import static com.yahoo.vespa.flags.FetchVector.Dimension.HOSTNAME; import static com.yahoo.vespa.flags.FetchVector.Dimension.TENANT_ID; import static com.yahoo.vespa.flags.FetchVector.Dimension.VESPA_VERSION; @@ -79,7 +80,7 @@ public class Flags { ZONE_ID, APPLICATION_ID); public static final UnboundBooleanFlag ENFORCE_RANK_PROFILE_INHERITANCE = defineFeatureFlag( - "enforce-rank-profile-inheritance", false, + "enforce-rank-profile-inheritance", true, List.of("baldersheim"), "2021-09-07", "2021-10-01", "Should we enforce verification of rank-profile inheritance.", "Takes effect at redeployment", @@ -171,6 +172,20 @@ public class Flags { "Takes effect at redeployment", ZONE_ID, APPLICATION_ID); + public static final UnboundBooleanFlag CONTAINER_DUMP_HEAP_ON_SHUTDOWN_TIMEOUT = defineFeatureFlag( + "container-dump-heap-on-shutdown-timeout", false, + List.of("baldersheim"), "2021-09-25", "2021-11-01", + "Will trigger a heap dump during if container shutdown times out", + "Takes effect at redeployment", + ZONE_ID, APPLICATION_ID); + + public static final UnboundDoubleFlag CONTAINER_SHUTDOWN_TIMEOUT = defineDoubleFlag( + "container-shutdown-timeout", 50.0, + List.of("baldersheim"), "2021-09-25", "2021-11-01", + "Timeout for shutdown of a jdisc container", + "Takes effect at redeployment", + ZONE_ID, APPLICATION_ID); + public static final UnboundListFlag<String> ALLOWED_ATHENZ_PROXY_IDENTITIES = defineListFlag( "allowed-athenz-proxy-identities", List.of(), String.class, List.of("bjorncs", "tokle"), "2021-02-10", "2021-12-01", @@ -278,7 +293,7 @@ public class Flags { List.of("olaa"), "2021-09-13", "2021-12-31", "Enable Horizon dashboard", "Takes effect immediately", - TENANT_ID + TENANT_ID, CONSOLE_USER_EMAIL ); public static final UnboundBooleanFlag ENABLE_ONPREM_TENANT_S3_ARCHIVE = defineFeatureFlag( @@ -416,8 +431,8 @@ public class Flags { * * <p>NOT thread-safe. Tests using this cannot run in parallel. */ - public static Replacer clearFlagsForTesting() { - return new Replacer(); + public static Replacer clearFlagsForTesting(FlagId... flagsToKeep) { + return new Replacer(flagsToKeep); } public static class Replacer implements AutoCloseable { @@ -425,10 +440,11 @@ public class Flags { private final TreeMap<FlagId, FlagDefinition> savedFlags; - private Replacer() { + private Replacer(FlagId... flagsToKeep) { verifyAndSetFlagsCleared(true); this.savedFlags = Flags.flags; Flags.flags = new TreeMap<>(); + List.of(flagsToKeep).forEach(id -> Flags.flags.put(id, savedFlags.get(id))); } @Override diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java index 46961fbd8cc..f73e0033773 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java @@ -52,6 +52,16 @@ public interface Condition extends Predicate<FetchVector> { public FetchVector.Dimension dimension() { return dimension; } public List<String> values() { return values; } public Optional<String> predicate() { return predicate; } + + public Condition createAs(Condition.Type type) { + switch (type) { + case WHITELIST: return WhitelistCondition.create(this); + case BLACKLIST: return BlacklistCondition.create(this); + case RELATIONAL: return RelationalCondition.create(this); + } + + throw new IllegalArgumentException("Unknown type '" + type + "'"); + } } static Condition fromWire(WireCondition wireCondition) { @@ -70,14 +80,14 @@ public interface Condition extends Predicate<FetchVector> { params.withPredicate(wireCondition.predicate); } - switch (type) { - case WHITELIST: return WhitelistCondition.create(params); - case BLACKLIST: return BlacklistCondition.create(params); - case RELATIONAL: return RelationalCondition.create(params); - } - - throw new IllegalArgumentException("Unknown type '" + type + "'"); + return params.createAs(type); } + Condition.Type type(); + + FetchVector.Dimension dimension(); + + CreateParams toCreateParams(); + WireCondition toWire(); } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java b/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java index c4079380a8c..eea61eb71ef 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java @@ -14,9 +14,6 @@ import com.yahoo.vespa.flags.json.wire.WireRule; import javax.annotation.concurrent.Immutable; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -35,16 +32,16 @@ public class FlagData { private final FetchVector defaultFetchVector; public FlagData(FlagId id) { - this(id, new FetchVector(), Collections.emptyList()); + this(id, new FetchVector(), List.of()); } public FlagData(FlagId id, FetchVector defaultFetchVector, Rule... rules) { - this(id, defaultFetchVector, Arrays.asList(rules)); + this(id, defaultFetchVector, List.of(rules)); } public FlagData(FlagId id, FetchVector defaultFetchVector, List<Rule> rules) { this.id = id; - this.rules = Collections.unmodifiableList(new ArrayList<>(rules)); + this.rules = List.copyOf(rules); this.defaultFetchVector = defaultFetchVector; } @@ -52,6 +49,10 @@ public class FlagData { return id; } + public List<Rule> rules() { + return rules; + } + public boolean isEmpty() { return rules.isEmpty() && defaultFetchVector.isEmpty(); } public Optional<RawFlag> resolve(FetchVector fetchVector) { @@ -136,7 +137,7 @@ public class FlagData { } private static List<Rule> rulesFromWire(List<WireRule> wireRules) { - if (wireRules == null) return Collections.emptyList(); + if (wireRules == null) return List.of(); return wireRules.stream().map(Rule::fromWire).collect(Collectors.toList()); } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java index c2c76529833..136857bea5f 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/ListCondition.java @@ -27,6 +27,21 @@ public abstract class ListCondition implements Condition { } @Override + public Type type() { + return type; + } + + @Override + public FetchVector.Dimension dimension() { + return dimension; + } + + @Override + public CreateParams toCreateParams() { + return new CreateParams(dimension).withValues(values); + } + + @Override public boolean test(FetchVector fetchVector) { boolean listContainsValue = fetchVector.getValue(dimension).map(values::contains).orElse(false); return isWhitelist == listContainsValue; diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java index db2f0a3a197..4ed3e49029f 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/RelationalCondition.java @@ -48,6 +48,21 @@ public class RelationalCondition implements Condition { } @Override + public Type type() { + return Type.RELATIONAL; + } + + @Override + public FetchVector.Dimension dimension() { + return dimension; + } + + @Override + public CreateParams toCreateParams() { + return new CreateParams(dimension).withPredicate(relationalPredicate.toWire()); + } + + @Override public boolean test(FetchVector fetchVector) { return fetchVector.getValue(dimension).map(predicate::test).orElse(false); } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java b/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java index b7d60889419..0d50f1e283f 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java @@ -6,7 +6,6 @@ import com.yahoo.vespa.flags.JsonNodeRawFlag; import com.yahoo.vespa.flags.RawFlag; import com.yahoo.vespa.flags.json.wire.WireRule; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -20,18 +19,32 @@ public class Rule { private final Optional<RawFlag> valueToApply; public Rule(Optional<RawFlag> valueToApply, Condition... andConditions) { - this(valueToApply, Arrays.asList(andConditions)); + this(valueToApply, List.of(andConditions)); } public Rule(Optional<RawFlag> valueToApply, List<Condition> andConditions) { - this.andConditions = andConditions; + this.andConditions = List.copyOf(andConditions); this.valueToApply = valueToApply; } + public List<Condition> conditions() { + return andConditions; + } + + /** Returns true if all the conditions satisfy the given fetch vector */ public boolean match(FetchVector fetchVector) { return andConditions.stream().allMatch(condition -> condition.test(fetchVector)); } + /** + * Returns true if all the conditions on dimensions set in the fetch vector are satisfied. + * Conditions on dimensions not specified in the given fetch vector are ignored. + */ + public boolean partialMatch(FetchVector fetchVector) { + return andConditions.stream() + .allMatch(condition -> !fetchVector.hasDimension(condition.dimension()) || condition.test(fetchVector)); + } + public Optional<RawFlag> getValueToApply() { return valueToApply; } diff --git a/functions.cmake b/functions.cmake index fe59cc3aaa9..3d192552be5 100644 --- a/functions.cmake +++ b/functions.cmake @@ -125,17 +125,13 @@ function(vespa_generate_config TARGET RELATIVE_CONFIG_DEF_PATH) get_filename_component(CONFIG_NAME ${RELATIVE_CONFIG_DEF_PATH} NAME_WE) endif() - # configgen.jar takes the parent dir of the destination dir and the destination dirname as separate parameters - # so it can produce the correct include statements within the generated .cpp-file (silent cry) # Make config path an absolute_path set(CONFIG_DEF_PATH ${CMAKE_CURRENT_LIST_DIR}/${RELATIVE_CONFIG_DEF_PATH}) - # Config destination is the + # Config destination is the current source directory (or parallel in build tree) + # configgen.jar takes the destination dirname as a property parameter set(CONFIG_DEST_DIR ${CMAKE_CURRENT_BINARY_DIR}) - # Get parent of destination directory - set(CONFIG_DEST_PARENT_DIR ${CONFIG_DEST_DIR}/..) - # Get destination dirname get_filename_component(CONFIG_DEST_DIRNAME ${CMAKE_CURRENT_BINARY_DIR} NAME) @@ -144,8 +140,8 @@ function(vespa_generate_config TARGET RELATIVE_CONFIG_DEF_PATH) add_custom_command( OUTPUT ${CONFIG_H_PATH} ${CONFIG_CPP_PATH} - COMMAND java -Dconfig.spec=${CONFIG_DEF_PATH} -Dconfig.dest=${CONFIG_DEST_PARENT_DIR} -Dconfig.lang=cpp -Dconfig.subdir=${CONFIG_DEST_DIRNAME} -Dconfig.dumpTree=false -Xms64m -Xmx64m -jar ${PROJECT_SOURCE_DIR}/configgen/target/configgen.jar - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.. + COMMAND java -Dconfig.spec=${CONFIG_DEF_PATH} -Dconfig.dest=${CONFIG_DEST_DIR} -Dconfig.lang=cpp -Dconfig.dumpTree=false -Xms64m -Xmx64m -jar ${PROJECT_SOURCE_DIR}/configgen/target/configgen.jar + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Generating cpp config for ${CONFIG_NAME} in ${CMAKE_CURRENT_SOURCE_DIR}" MAIN_DEPENDENCY ${CONFIG_DEF_PATH} ) @@ -157,11 +153,6 @@ function(vespa_generate_config TARGET RELATIVE_CONFIG_DEF_PATH) # Add generated to sources for target target_sources(${TARGET} PRIVATE ${CONFIG_H_PATH} ${CONFIG_CPP_PATH}) - # Needed to be able to do a #include <CONFIG_DEST_DIRNAME/config-<name>.h> for this target - # This is used within the generated config-<name>.cpp - # TODO: Should modify configgen to use #include <vespa/<modulename>/config-<name>.h> instead - target_include_directories(${TARGET} PRIVATE ${CONFIG_DEST_PARENT_DIR}) - # Needed to be able to do a #include <config-<name>.h> for this target # This is used within some unit tests target_include_directories(${TARGET} PRIVATE ${CONFIG_DEST_DIR}) diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerWatchdog.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerWatchdog.java index 30aa0028465..7e82955eede 100644 --- a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerWatchdog.java +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerWatchdog.java @@ -22,8 +22,9 @@ import java.util.logging.Logger; */ class ContainerWatchdog implements ContainerWatchdogMetrics, AutoCloseable { - static final Duration GRACE_PERIOD = Duration.ofMinutes(30); + static final Duration GRACE_PERIOD = Duration.ofMinutes(5); static final Duration UPDATE_PERIOD = Duration.ofMinutes(5); + static final Duration CHECK_PERIOD = Duration.ofSeconds(1); private static final Logger log = Logger.getLogger(ContainerWatchdog.class.getName()); @@ -35,6 +36,7 @@ class ContainerWatchdog implements ContainerWatchdogMetrics, AutoCloseable { private ActiveContainer currentContainer; private Instant currentContainerActivationTime; private int numStaleContainers; + private Instant lastLogTime; ContainerWatchdog() { this(new ScheduledThreadPoolExecutor( @@ -50,8 +52,8 @@ class ContainerWatchdog implements ContainerWatchdogMetrics, AutoCloseable { ContainerWatchdog(ScheduledExecutorService scheduler, Clock clock) { this.scheduler = scheduler; this.clock = clock; - scheduler.scheduleAtFixedRate( - this::monitorDeactivatedContainers, UPDATE_PERIOD.getSeconds(), UPDATE_PERIOD.getSeconds(), TimeUnit.SECONDS); + this.lastLogTime = clock.instant(); + scheduler.scheduleAtFixedRate(this::monitorDeactivatedContainers, CHECK_PERIOD.getSeconds(), CHECK_PERIOD.getSeconds(), TimeUnit.SECONDS); } @Override @@ -77,31 +79,43 @@ class ContainerWatchdog implements ContainerWatchdogMetrics, AutoCloseable { void onContainerActivation(ActiveContainer nextContainer) { synchronized (monitor) { if (currentContainer != null) { - deactivatedContainers.add( - new DeactivatedContainer(currentContainer, currentContainerActivationTime, clock.instant())); + deactivatedContainers.add(new DeactivatedContainer(currentContainer, currentContainerActivationTime, clock.instant())); } currentContainer = nextContainer; currentContainerActivationTime = clock.instant(); } } + private String removalMsg(DeactivatedContainer container) { + return String.format("Removing deactivated container: instance=%s, activated=%s, deactivated=%s", + container.instance, container.timeActivated, container.timeDeactivated); + } + + private String regularMsg(DeactivatedContainer container, int refCount) { + return String.format( + "Deactivated container still alive: instance=%s, activated=%s, deactivated=%s, ref-count=%d", + container.instance, container.timeActivated, container.timeDeactivated, refCount); + } + void monitorDeactivatedContainers() { synchronized (monitor) { int numStaleContainer = 0; Iterator<DeactivatedContainer> iterator = deactivatedContainers.iterator(); + boolean timeToLogAgain = clock.instant().isAfter(lastLogTime.plus(UPDATE_PERIOD)); while (iterator.hasNext()) { DeactivatedContainer container = iterator.next(); int refCount = container.instance.retainCount(); if (refCount == 0) { + log.fine(removalMsg(container)); iterator.remove(); - break; - } - if (isPastGracePeriod(container)) { - ++numStaleContainer; - log.warning( - String.format( - "Deactivated container still alive: instance=%s, activated=%s, deactivated=%s, ref-count=%d", - container.instance.toString(), container.timeActivated, container.timeDeactivated, refCount)); + } else { + if (isPastGracePeriod(container)) { + ++numStaleContainer; + if (timeToLogAgain) { + log.warning(regularMsg(container, refCount)); + lastLogTime = clock.instant(); + } + } } } this.numStaleContainers = numStaleContainer; diff --git a/logd/src/logd/log_protocol_proto.h b/logd/src/logd/log_protocol_proto.h index 6f82b5d28b8..2fcb7d03919 100644 --- a/logd/src/logd/log_protocol_proto.h +++ b/logd/src/logd/log_protocol_proto.h @@ -7,7 +7,7 @@ #pragma GCC diagnostic ignored "-Wsuggest-override" #endif -#include "log_protocol.pb.h" +#include <logd/log_protocol.pb.h> #pragma GCC diagnostic pop diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/HttpMetricFetcher.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/HttpMetricFetcher.java index b4c116d1903..117503b95dc 100644 --- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/HttpMetricFetcher.java +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/HttpMetricFetcher.java @@ -32,7 +32,7 @@ import java.util.logging.Logger; */ public abstract class HttpMetricFetcher { - private final static Logger log = Logger.getLogger(HttpMetricFetcher.class.getPackage().getName()); + private final static Logger log = Logger.getLogger(HttpMetricFetcher.class.getName()); public final static String STATE_PATH = "/state/v1/"; // The call to apache will do 3 retries. As long as we check the services in series, we can't have this too high. public static int CONNECTION_TIMEOUT = 5000; diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/RemoteHealthMetricFetcher.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/RemoteHealthMetricFetcher.java index 59db14670aa..03e72ec36de 100644 --- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/RemoteHealthMetricFetcher.java +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/RemoteHealthMetricFetcher.java @@ -5,7 +5,6 @@ import ai.vespa.metricsproxy.metric.HealthMetric; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.concurrent.ExecutionException; import java.util.logging.Level; @@ -36,19 +35,19 @@ public class RemoteHealthMetricFetcher extends HttpMetricFetcher { return createHealthMetrics(stream, fetchCount); } catch (IOException | InterruptedException | ExecutionException e) { logMessageNoResponse(errMsgNoResponse(e), fetchCount); - byte [] empty = {'{','}'}; - return createHealthMetrics(new ByteArrayInputStream(empty), fetchCount); + return HealthMetric.getUnknown("Failed fetching metrics for service: " + service.getMonitoringName()); } } /** * Connect to remote service over http and fetch metrics */ - private HealthMetric createHealthMetrics(InputStream data, int fetchCount) { + private HealthMetric createHealthMetrics(InputStream data, int fetchCount) throws IOException { try { return parse(data); } catch (Exception e) { handleException(e, data, fetchCount); + while (data.read() != -1) {} return HealthMetric.getDown("Failed fetching status page for service"); } } diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/RemoteMetricsFetcher.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/RemoteMetricsFetcher.java index 3ee1e05c263..aad2f816959 100644 --- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/RemoteMetricsFetcher.java +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/service/RemoteMetricsFetcher.java @@ -35,11 +35,12 @@ public class RemoteMetricsFetcher extends HttpMetricFetcher { handleException(e, data, fetchCount); } } - private void createMetrics(InputStream data, MetricsParser.Consumer consumer, int fetchCount) { + private void createMetrics(InputStream data, MetricsParser.Consumer consumer, int fetchCount) throws IOException { try { MetricsParser.parse(data, consumer); } catch (Exception e) { handleException(e, data, fetchCount); + while (data.read() != -1) {} } } } diff --git a/metrics/src/vespa/metrics/metricmanager.h b/metrics/src/vespa/metrics/metricmanager.h index f5ad5c5eea3..15f81126669 100644 --- a/metrics/src/vespa/metrics/metricmanager.h +++ b/metrics/src/vespa/metrics/metricmanager.h @@ -43,7 +43,7 @@ */ #pragma once -#include "config-metricsmanager.h" +#include <vespa/metrics/config-metricsmanager.h> #include "metricset.h" #include "metricsnapshot.h" #include "memoryconsumption.h" diff --git a/model-integration/pom.xml b/model-integration/pom.xml index dc3154c5c41..62014ef174a 100644 --- a/model-integration/pom.xml +++ b/model-integration/pom.xml @@ -106,29 +106,4 @@ </plugins> </build> - <profiles> - <!-- Exclude TF JNI when building for rhel6, which needs a special, natively installed variant --> - <profile> - <id>rhel6</id> - <activation> - <property> - <name>target.env</name> - <value>rhel6</value> - </property> - </activation> - <dependencies> - <dependency> - <groupId>org.tensorflow</groupId> - <artifactId>tensorflow</artifactId> - <exclusions> - <exclusion> - <groupId>org.tensorflow</groupId> - <artifactId>libtensorflow_jni</artifactId> - </exclusion> - </exclusions> - </dependency> - </dependencies> - </profile> - </profiles> - </project> diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeAttributes.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeAttributes.java index 2379659f74b..32336743bdc 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeAttributes.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeAttributes.java @@ -6,9 +6,11 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.DockerImage; import java.time.Instant; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -29,6 +31,7 @@ public class NodeAttributes { private Optional<Version> vespaVersion = Optional.empty(); private Optional<Version> currentOsVersion = Optional.empty(); private Optional<Instant> currentFirmwareCheck = Optional.empty(); + private List<TrustStoreItem> trustStore = List.of(); /** The list of reports to patch. A null value is used to remove the report. */ private Map<String, JsonNode> reports = new TreeMap<>(); @@ -144,7 +147,17 @@ public class NodeAttributes { && Objects.equals(vespaVersion, other.vespaVersion) && Objects.equals(currentOsVersion, other.currentOsVersion) && Objects.equals(currentFirmwareCheck, other.currentFirmwareCheck) - && Objects.equals(reports, other.reports); + && Objects.equals(reports, other.reports) + && Objects.equals(trustStore, other.trustStore); + } + + public NodeAttributes withTrustStore(List<TrustStoreItem> trustStore) { + this.trustStore = List.copyOf(trustStore); + return this; + } + + public List<TrustStoreItem> getTrustStore() { + return trustStore; } @Override @@ -156,7 +169,8 @@ public class NodeAttributes { vespaVersion.map(ver -> "vespaVersion=" + ver.toFullString()), currentOsVersion.map(ver -> "currentOsVersion=" + ver.toFullString()), currentFirmwareCheck.map(at -> "currentFirmwareCheck=" + at), - Optional.ofNullable(reports.isEmpty() ? null : "reports=" + reports)) + Optional.ofNullable(reports.isEmpty() ? null : "reports=" + reports), + Optional.ofNullable(trustStore.isEmpty() ? null : "trustStore=" + trustStore)) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.joining(", ", "{", "}")); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeSpec.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeSpec.java index e85d51ef992..4000a0ac182 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeSpec.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeSpec.java @@ -68,6 +68,8 @@ public class NodeSpec { private final Optional<ApplicationId> exclusiveTo; + private final List<TrustStoreItem> trustStore; + public NodeSpec( String hostname, Optional<String> id, @@ -98,7 +100,8 @@ public class NodeSpec { List<Event> events, Optional<String> parentHostname, Optional<URI> archiveUri, - Optional<ApplicationId> exclusiveTo) { + Optional<ApplicationId> exclusiveTo, + List<TrustStoreItem> trustStore) { if (state == NodeState.active) { requireOptional(owner, "owner"); requireOptional(membership, "membership"); @@ -138,6 +141,7 @@ public class NodeSpec { this.parentHostname = Objects.requireNonNull(parentHostname); this.archiveUri = Objects.requireNonNull(archiveUri); this.exclusiveTo = Objects.requireNonNull(exclusiveTo); + this.trustStore = Objects.requireNonNull(trustStore); } public String hostname() { @@ -283,6 +287,10 @@ public class NodeSpec { return exclusiveTo; } + public List<TrustStoreItem> trustStore() { + return trustStore; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -319,7 +327,8 @@ public class NodeSpec { Objects.equals(events, that.events) && Objects.equals(parentHostname, that.parentHostname) && Objects.equals(archiveUri, that.archiveUri) && - Objects.equals(exclusiveTo, that.exclusiveTo); + Objects.equals(exclusiveTo, that.exclusiveTo) && + Objects.equals(trustStore, that.trustStore); } @Override @@ -354,7 +363,8 @@ public class NodeSpec { events, parentHostname, archiveUri, - exclusiveTo); + exclusiveTo, + trustStore); } @Override @@ -390,6 +400,7 @@ public class NodeSpec { + " parentHostname=" + parentHostname + " archiveUri=" + archiveUri + " exclusiveTo=" + exclusiveTo + + " trustStore=" + trustStore + " }"; } @@ -424,6 +435,7 @@ public class NodeSpec { private Optional<String> parentHostname = Optional.empty(); private Optional<URI> archiveUri = Optional.empty(); private Optional<ApplicationId> exclusiveTo = Optional.empty(); + private List<TrustStoreItem> trustStore = List.of(); public Builder() {} @@ -456,6 +468,7 @@ public class NodeSpec { node.parentHostname.ifPresent(this::parentHostname); node.archiveUri.ifPresent(this::archiveUri); node.exclusiveTo.ifPresent(this::exclusiveTo); + trustStore(node.trustStore); } public Builder hostname(String hostname) { @@ -633,12 +646,19 @@ public class NodeSpec { return this; } + public Builder trustStore(List<TrustStoreItem> trustStore) { + this.trustStore = List.copyOf(trustStore); + return this; + } + public Builder updateFromNodeAttributes(NodeAttributes attributes) { attributes.getHostId().ifPresent(this::id); attributes.getDockerImage().ifPresent(this::currentDockerImage); attributes.getCurrentOsVersion().ifPresent(this::currentOsVersion); attributes.getRebootGeneration().ifPresent(this::currentRebootGeneration); attributes.getRestartGeneration().ifPresent(this::currentRestartGeneration); + // Always replace entire trust store + trustStore(attributes.getTrustStore()); this.reports.updateFromRawMap(attributes.getReports()); return this; @@ -752,7 +772,7 @@ public class NodeSpec { wantedRebootGeneration, currentRebootGeneration, wantedFirmwareCheck, currentFirmwareCheck, modelName, resources, realResources, ipAddresses, additionalIpAddresses, - reports, events, parentHostname, archiveUri, exclusiveTo); + reports, events, parentHostname, archiveUri, exclusiveTo, trustStore); } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/RealNodeRepository.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/RealNodeRepository.java index abc779d8a9a..d5e3acf0656 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/RealNodeRepository.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/RealNodeRepository.java @@ -155,6 +155,11 @@ public class RealNodeRepository implements NodeRepository { .map(event -> new Event(event.agent, event.event, Optional.ofNullable(event.at).map(Instant::ofEpochMilli).orElse(Instant.EPOCH))) .collect(Collectors.toUnmodifiableList()); + List<TrustStoreItem> trustStore = Optional.ofNullable(node.trustStore).orElse(List.of()).stream() + .map(item -> new TrustStoreItem(item.fingerprint, Instant.ofEpochMilli(item.expiry))) + .collect(Collectors.toList()); + + return new NodeSpec( node.hostname, Optional.ofNullable(node.openStackId), @@ -185,7 +190,8 @@ public class RealNodeRepository implements NodeRepository { events, Optional.ofNullable(node.parentHostname), Optional.ofNullable(node.archiveUri).map(URI::create), - Optional.ofNullable(node.exclusiveTo).map(ApplicationId::fromSerializedForm)); + Optional.ofNullable(node.exclusiveTo).map(ApplicationId::fromSerializedForm), + trustStore); } private static NodeResources nodeResources(NodeRepositoryNode.NodeResources nodeResources) { @@ -270,7 +276,9 @@ public class RealNodeRepository implements NodeRepository { node.vespaVersion = nodeAttributes.getVespaVersion().map(Version::toFullString).orElse(null); node.currentOsVersion = nodeAttributes.getCurrentOsVersion().map(Version::toFullString).orElse(null); node.currentFirmwareCheck = nodeAttributes.getCurrentFirmwareCheck().map(Instant::toEpochMilli).orElse(null); - + node.trustStore = nodeAttributes.getTrustStore().stream() + .map(item -> new NodeRepositoryNode.TrustStoreItem(item.fingerprint(), item.expiry().toEpochMilli())) + .collect(Collectors.toList()); Map<String, JsonNode> reports = nodeAttributes.getReports(); node.reports = reports == null || reports.isEmpty() ? null : new TreeMap<>(reports); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/TrustStoreItem.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/TrustStoreItem.java new file mode 100644 index 00000000000..d3e797bca24 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/TrustStoreItem.java @@ -0,0 +1,48 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.node.admin.configserver.noderepository; + +import java.time.Instant; +import java.util.Objects; + +/** + * @author mortent + */ +public class TrustStoreItem { + private final String fingerprint; + private final Instant expiry; + + public TrustStoreItem(String fingerprint, Instant expiry) { + this.fingerprint = fingerprint; + this.expiry = expiry; + } + + public String fingerprint() { + return fingerprint; + } + + public Instant expiry() { + return expiry; + } + + @Override + public String toString() { + return "TrustStoreItem{" + + "fingerprint='" + fingerprint + '\'' + + ", expiry=" + expiry + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TrustStoreItem that = (TrustStoreItem) o; + return Objects.equals(fingerprint, that.fingerprint) && Objects.equals(expiry, that.expiry); + } + + @Override + public int hashCode() { + return Objects.hash(fingerprint, expiry); + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/NodeRepositoryNode.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/NodeRepositoryNode.java index 4282c67b4cd..f471f9a9965 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/NodeRepositoryNode.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/NodeRepositoryNode.java @@ -85,6 +85,9 @@ public class NodeRepositoryNode { public String exclusiveTo; @JsonProperty("history") public List<Event> history; + @JsonProperty("trustStore") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public List<TrustStoreItem> trustStore; @JsonProperty("reports") public Map<String, JsonNode> reports = null; @@ -221,4 +224,17 @@ public class NodeRepositoryNode { '}'; } } + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class TrustStoreItem { + @JsonProperty ("fingerprint") + public String fingerprint; + @JsonProperty ("expiry") + public long expiry; + + public TrustStoreItem(@JsonProperty("fingerprint") String fingerprint, @JsonProperty("expiry") long expiry) { + this.fingerprint = fingerprint; + this.expiry = expiry; + } + } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java index 240e041a504..df3ac00ce7f 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java @@ -18,13 +18,16 @@ import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.NodeAcl; import com.yahoo.vespa.hosted.provision.node.Reports; import com.yahoo.vespa.hosted.provision.node.Status; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import java.time.Instant; import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; /** * A node in the node repository. The identity of a node is given by its id. @@ -50,6 +53,7 @@ public final class Node implements Nodelike { private final Optional<ApplicationId> exclusiveToApplicationId; private final Optional<ClusterSpec.Type> exclusiveToClusterType; private final Optional<String> switchHostname; + private final List<TrustStoreItem> trustStoreItems; /** Record of the last event of each type happening to this node */ private final History history; @@ -79,7 +83,7 @@ public final class Node implements Nodelike { Flavor flavor, Status status, State state, Optional<Allocation> allocation, History history, NodeType type, Reports reports, Optional<String> modelName, Optional<TenantName> reservedTo, Optional<ApplicationId> exclusiveToApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType, - Optional<String> switchHostname) { + Optional<String> switchHostname, List<TrustStoreItem> trustStoreItems) { this.id = Objects.requireNonNull(id, "A node must have an ID"); this.hostname = requireNonEmptyString(hostname, "A node must have a hostname"); this.ipConfig = Objects.requireNonNull(ipConfig, "A node must a have an IP config"); @@ -96,6 +100,7 @@ public final class Node implements Nodelike { this.exclusiveToApplicationId = Objects.requireNonNull(exclusiveToApplicationId, "exclusiveToApplicationId cannot be null"); this.exclusiveToClusterType = Objects.requireNonNull(exclusiveToClusterType, "exclusiveToClusterType cannot be null"); this.switchHostname = requireNonEmptyString(switchHostname, "switchHostname cannot be null"); + this.trustStoreItems = trustStoreItems.stream().distinct().collect(Collectors.toUnmodifiableList()); if (state == State.active) requireNonEmpty(ipConfig.primary(), "Active node " + hostname + " must have at least one valid IP address"); @@ -207,6 +212,11 @@ public final class Node implements Nodelike { return switchHostname; } + /** Returns the trusted certificates for this host if any. */ + public List<TrustStoreItem> trustedCertificates() { + return trustStoreItems; + } + /** * Returns a copy of this where wantToFail is set to true and history is updated to reflect this. */ @@ -295,13 +305,13 @@ public final class Node implements Nodelike { /** Returns a node with the status assigned to the given value */ public Node with(Status status) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, - reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a node with the type assigned to the given value */ public Node with(NodeType type) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, - reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a node with the flavor assigned to the given value */ @@ -309,31 +319,31 @@ public final class Node implements Nodelike { if (flavor.equals(this.flavor)) return this; History updateHistory = history.with(new History.Event(History.Event.Type.resized, agent, instant)); return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, updateHistory, type, - reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with the reboot generation set to generation */ public Node withReboot(Generation generation) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status.withReboot(generation), state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with the openStackId set */ public Node withOpenStackId(String openStackId) { return new Node(openStackId, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with model name set to given value */ public Node withModelName(String modelName) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, Optional.of(modelName), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, Optional.of(modelName), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with model name cleared */ public Node withoutModelName() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, Optional.empty(), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, Optional.empty(), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this with a history record saying it was detected to be down at this instant */ @@ -364,55 +374,55 @@ public final class Node implements Nodelike { */ public Node with(Allocation allocation) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a new Node without an allocation. */ public Node withoutAllocation() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - Optional.empty(), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + Optional.empty(), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this node with IP config set to the given value. */ public Node with(IP.Config ipConfig) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this node with the parent hostname assigned to the given value. */ public Node withParentHostname(String parentHostname) { return new Node(id, ipConfig, hostname, Optional.of(parentHostname), flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } public Node withReservedTo(TenantName tenant) { if (type != NodeType.host) throw new IllegalArgumentException("Only host nodes can be reserved, " + hostname + " has type " + type); return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } /** Returns a copy of this node which is not reserved to a tenant */ public Node withoutReservedTo() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, Optional.empty(), exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, Optional.empty(), exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } public Node withExclusiveToApplicationId(ApplicationId exclusiveTo) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), exclusiveToClusterType, switchHostname, trustStoreItems); } public Node withExclusiveToClusterType(ClusterSpec.Type exclusiveTo) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, Optional.ofNullable(exclusiveTo), switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, Optional.ofNullable(exclusiveTo), switchHostname, trustStoreItems); } /** Returns a copy of this node with switch hostname set to given value */ public Node withSwitchHostname(String switchHostname) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, Optional.ofNullable(switchHostname)); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, Optional.ofNullable(switchHostname), trustStoreItems); } /** Returns a copy of this node with switch hostname unset */ @@ -450,12 +460,18 @@ public final class Node implements Nodelike { /** Returns a copy of this node with the given history. */ public Node with(History history) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); } public Node with(Reports reports) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + } + + public Node with(List<TrustStoreItem> trustStoreItems) { + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, + trustStoreItems); } private static Optional<String> requireNonEmptyString(Optional<String> value, String message) { @@ -594,6 +610,7 @@ public final class Node implements Nodelike { private Status status; private Reports reports; private History history; + private List<TrustStoreItem> trustStoreItems; private Builder(String id, String hostname, Flavor flavor, State state, NodeType type) { this.id = id; @@ -663,12 +680,18 @@ public final class Node implements Nodelike { return this; } + public Builder trustedCertificates(List<TrustStoreItem> trustStoreItems) { + this.trustStoreItems = trustStoreItems; + return this; + } + public Node build() { return new Node(id, Optional.ofNullable(ipConfig).orElse(IP.Config.EMPTY), hostname, Optional.ofNullable(parentHostname), - flavor, Optional.ofNullable(status).orElseGet(Status::initial), state, Optional.ofNullable(allocation), - Optional.ofNullable(history).orElseGet(History::empty), type, Optional.ofNullable(reports).orElseGet(Reports::new), - Optional.ofNullable(modelName), Optional.ofNullable(reservedTo), Optional.ofNullable(exclusiveToApplicationId), - Optional.ofNullable(exclusiveToClusterType), Optional.ofNullable(switchHostname)); + flavor, Optional.ofNullable(status).orElseGet(Status::initial), state, Optional.ofNullable(allocation), + Optional.ofNullable(history).orElseGet(History::empty), type, Optional.ofNullable(reports).orElseGet(Reports::new), + Optional.ofNullable(modelName), Optional.ofNullable(reservedTo), Optional.ofNullable(exclusiveToApplicationId), + Optional.ofNullable(exclusiveToClusterType), Optional.ofNullable(switchHostname), + Optional.ofNullable(trustStoreItems).orElseGet(List::of)); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/TrustStoreItem.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/TrustStoreItem.java new file mode 100644 index 00000000000..6fb94d0bc62 --- /dev/null +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/TrustStoreItem.java @@ -0,0 +1,68 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.provision.node; + +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Inspector; + +import java.time.Instant; +import java.util.Objects; + +/** + * Contains the fingerprint and expiry of certificates in a hosts truststore. + * + * @author mortent + */ +public class TrustStoreItem { + private static final String FINGERPRINT_FIELD = "fingerprint"; + private static final String EXPIRY_FIELD = "expiry"; + + private final String fingerprint; + private final Instant expiry; + + public TrustStoreItem(String fingerprint, Instant expiry) { + this.fingerprint = fingerprint; + this.expiry = expiry; + } + + public String fingerprint() { + return fingerprint; + } + + public Instant expiry() { + return expiry; + } + + public void toSlime(Cursor trustedCertificatesRoot) { + Cursor object = trustedCertificatesRoot.addObject(); + object.setString(FINGERPRINT_FIELD, fingerprint); + object.setLong(EXPIRY_FIELD, expiry.toEpochMilli()); + } + + public static TrustStoreItem fromSlime(Inspector inspector) { + String fingerprint = inspector.field(FINGERPRINT_FIELD).asString(); + Instant expiry = Instant.ofEpochMilli(inspector.field(EXPIRY_FIELD).asLong()); + return new TrustStoreItem(fingerprint, expiry); + } + + @Override + public String toString() { + return "TrustedCertificate{" + + "fingerprint='" + fingerprint + '\'' + + ", expiry=" + expiry + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TrustStoreItem that = (TrustStoreItem) o; + return Objects.equals(fingerprint, that.fingerprint) && Objects.equals(expiry, that.expiry); + } + + @Override + public int hashCode() { + return Objects.hash(fingerprint, expiry); + } +}
\ No newline at end of file diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java index d5a4c459ef3..7e252391cb3 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java @@ -210,7 +210,7 @@ public class CuratorDatabaseClient { toState.isAllocated() ? node.allocation() : Optional.empty(), node.history().recordStateTransition(node.state(), toState, agent, clock.instant()), node.type(), node.reports(), node.modelName(), node.reservedTo(), - node.exclusiveToApplicationId(), node.exclusiveToClusterType(), node.switchHostname()); + node.exclusiveToApplicationId(), node.exclusiveToClusterType(), node.switchHostname(), node.trustedCertificates()); writeNode(toState, curatorTransaction, node, newNode); writtenNodes.add(newNode); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java index 19bbe92eff6..868837daeeb 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java @@ -36,6 +36,7 @@ import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.OsVersion; import com.yahoo.vespa.hosted.provision.node.Reports; import com.yahoo.vespa.hosted.provision.node.Status; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -95,6 +96,7 @@ public class NodeSerializer { private static final String exclusiveToApplicationIdKey = "exclusiveTo"; private static final String exclusiveToClusterTypeKey = "exclusiveToClusterType"; private static final String switchHostnameKey = "switchHostname"; + private static final String trustedCertificatesKey = "trustedCertificates"; // Node resource fields private static final String flavorKey = "flavor"; @@ -122,6 +124,10 @@ public class NodeSerializer { // Network port fields private static final String networkPortsKey = "networkPorts"; + // Trusted certificates fields + private static final String fingerprintKey = "fingerprint"; + private static final String expiresKey = "expires"; + // A cache of deserialized Node objects. The cache is keyed on the hash of serialized node data. // // Deserializing a Node from slime is expensive, and happens frequently. Node instances that have already been @@ -182,6 +188,7 @@ public class NodeSerializer { node.reservedTo().ifPresent(tenant -> object.setString(reservedToKey, tenant.value())); node.exclusiveToApplicationId().ifPresent(applicationId -> object.setString(exclusiveToApplicationIdKey, applicationId.serializedForm())); node.exclusiveToClusterType().ifPresent(clusterType -> object.setString(exclusiveToClusterTypeKey, clusterType.name())); + trustedCertificatesToSlime(node.trustedCertificates(), object.setArray(trustedCertificatesKey)); } private void toSlime(Flavor flavor, Cursor object) { @@ -236,6 +243,14 @@ public class NodeSerializer { }); } + private void trustedCertificatesToSlime(List<TrustStoreItem> trustStoreItems, Cursor array) { + trustStoreItems.forEach(cert -> { + Cursor object = array.addObject(); + object.setString(fingerprintKey, cert.fingerprint()); + object.setLong(expiresKey, cert.expiry().toEpochMilli()); + }); + } + // ---------------- Deserialization -------------------------------------------------- public Node fromJson(Node.State state, byte[] data) { @@ -269,7 +284,8 @@ public class NodeSerializer { reservedToFromSlime(object.field(reservedToKey)), exclusiveToApplicationIdFromSlime(object.field(exclusiveToApplicationIdKey)), exclusiveToClusterTypeFromSlime(object.field(exclusiveToClusterTypeKey)), - switchHostnameFromSlime(object.field(switchHostnameKey))); + switchHostnameFromSlime(object.field(switchHostnameKey)), + trustedCertificatesFromSlime(object)); } private Status statusFromSlime(Inspector object) { @@ -419,6 +435,13 @@ public class NodeSerializer { return Optional.of(ClusterSpec.Type.from(object.asString())); } + private List<TrustStoreItem> trustedCertificatesFromSlime(Inspector object) { + return SlimeUtils.entriesStream(object.field(trustedCertificatesKey)) + .map(elem -> new TrustStoreItem(elem.field(fingerprintKey).asString(), + Instant.ofEpochMilli(elem.field(expiresKey).asLong()))) + .collect(Collectors.toList()); + } + // ----------------- Enum <-> string mappings ---------------------------------------- /** Returns the event type, or null if this event type should be ignored */ diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java index 8d37c13d2bc..5dfacc2c3d2 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java @@ -25,6 +25,7 @@ import com.yahoo.vespa.hosted.provision.node.Allocation; import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.Report; import com.yahoo.vespa.hosted.provision.node.Reports; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import java.io.IOException; import java.io.InputStream; @@ -186,6 +187,8 @@ public class NodePatcher implements AutoCloseable { return node.withExclusiveToClusterType(SlimeUtils.optionalString(value).map(ClusterSpec.Type::valueOf).orElse(null)); case "switchHostname": return value.type() == Type.NIX ? node.withoutSwitchHostname() : node.withSwitchHostname(value.asString()); + case "trustStore": + return nodeWithTrustStore(node, value); default : throw new IllegalArgumentException("Could not apply field '" + name + "' on a node: No such modifiable field"); } @@ -230,6 +233,14 @@ public class NodePatcher implements AutoCloseable { return patchedNode; } + private Node nodeWithTrustStore(Node node, Inspector inspector) { + List<TrustStoreItem> trustStoreItems = + SlimeUtils.entriesStream(inspector) + .map(TrustStoreItem::fromSlime) + .collect(Collectors.toList()); + return node.with(trustStoreItems); + } + private Set<String> asStringSet(Inspector field) { if ( ! field.type().equals(Type.ARRAY)) throw new IllegalArgumentException("Expected an ARRAY value, got a " + field.type()); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java index 2cf671514c4..80100128379 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java @@ -16,6 +16,7 @@ import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Address; import com.yahoo.vespa.hosted.provision.node.History; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import com.yahoo.vespa.orchestrator.Orchestrator; import com.yahoo.vespa.orchestrator.status.HostInfo; import com.yahoo.vespa.orchestrator.status.HostStatus; @@ -182,6 +183,7 @@ class NodesResponse extends SlimeJsonResponse { node.modelName().ifPresent(modelName -> object.setString("modelName", modelName)); node.switchHostname().ifPresent(switchHostname -> object.setString("switchHostname", switchHostname)); nodeRepository.archiveUris().archiveUriFor(node).ifPresent(uri -> object.setString("archiveUri", uri)); + trustedCertsToSlime(node.trustedCertificates(), object); } private void toSlime(ApplicationId id, Cursor object) { @@ -228,6 +230,12 @@ class NodesResponse extends SlimeJsonResponse { addresses.forEach(address -> addressesArray.addString(address.hostname())); } + private void trustedCertsToSlime(List<TrustStoreItem> trustStoreItems, Cursor object) { + if (trustStoreItems.isEmpty()) return; + Cursor array = object.setArray("trustStore"); + trustStoreItems.forEach(cert -> cert.toSlime(array)); + } + private String lastElement(String path) { if (path.endsWith("/")) path = path.substring(0, path.length()-1); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java index 158a1d6e5ac..921b78252d3 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java @@ -29,6 +29,7 @@ import com.yahoo.vespa.hosted.provision.node.History; import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.Report; import com.yahoo.vespa.hosted.provision.node.Reports; +import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import com.yahoo.vespa.hosted.provision.provisioning.FlavorConfigBuilder; import org.junit.Test; @@ -462,6 +463,16 @@ public class NodeSerializerTest { assertEquals(exclusiveToCluster, node.exclusiveToClusterType().get()); } + @Test + public void truststore_serialization() { + Node node = nodeSerializer.fromJson(State.active, nodeSerializer.toJson(createNode())); + assertEquals(List.of(), node.trustedCertificates()); + List<TrustStoreItem> trustStoreItems = List.of(new TrustStoreItem("foo", Instant.parse("2023-09-01T23:59:59Z")), new TrustStoreItem("bar", Instant.parse("2025-05-20T23:59:59Z"))); + node = node.with(trustStoreItems); + node = nodeSerializer.fromJson(State.active, nodeSerializer.toJson(node)); + assertEquals(trustStoreItems, node.trustedCertificates()); + } + private byte[] createNodeJson(String hostname, String... ipAddress) { String ipAddressJsonPart = ""; if (ipAddress.length > 0) { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java index fa68533bb06..4c9597d00bd 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java @@ -43,7 +43,6 @@ import com.yahoo.vespa.hosted.provision.node.filter.NodeHostFilter; import com.yahoo.vespa.hosted.provision.persistence.NameResolver; import com.yahoo.vespa.hosted.provision.testutils.MockNameResolver; import com.yahoo.vespa.hosted.provision.testutils.MockProvisionServiceProvider; -import com.yahoo.vespa.orchestrator.OrchestrationException; import com.yahoo.vespa.orchestrator.Orchestrator; import com.yahoo.vespa.service.duper.ConfigServerApplication; @@ -687,11 +686,7 @@ public class ProvisioningTester { Orchestrator orchestrator = Optional.ofNullable(this.orchestrator) .orElseGet(() -> { Orchestrator orch = mock(Orchestrator.class); - try { - doThrow(new RuntimeException()).when(orch).acquirePermissionToRemove(any()); - } catch (OrchestrationException e) { - throw new RuntimeException(e); - } + doThrow(new RuntimeException()).when(orch).acquirePermissionToRemove(any()); return orch; }); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java index 6c052a6c364..88a8a22913e 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java @@ -1023,6 +1023,26 @@ public class NodesV2ApiTest { tester.assertPartialResponse(new Request("http://localhost:8080/nodes/v2/node/host4.yahoo.com"), "archiveUri", false); } + @Test + public void trusted_certificates_patch() throws IOException { + String url = "http://localhost:8080/nodes/v2/node/dockerhost1.yahoo.com"; + tester.assertPartialResponse(new Request(url), "\"trustStore\":[]", false); // initially empty list + + String trustStore = "\"trustStore\":[" + + "{" + + "\"fingerprint\":\"foo\"," + + "\"expiry\":1632302251000" + + "}," + + "{" + + "\"fingerprint\":\"bar\"," + + "\"expiry\":1758532706000" + + "}" + + "]"; + assertResponse(new Request(url, Utf8.toBytes("{"+trustStore+"}"), Request.Method.PATCH), + "{\"message\":\"Updated dockerhost1.yahoo.com\"}"); + tester.assertPartialResponse(new Request(url), trustStore, true); + } + private static String asDockerNodeJson(String hostname, String parentHostname, String... ipAddress) { return asDockerNodeJson(hostname, NodeType.tenant, parentHostname, ipAddress); } diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestrationException.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestrationException.java index 95d862d9e72..f2eae278150 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestrationException.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestrationException.java @@ -3,7 +3,7 @@ package com.yahoo.vespa.orchestrator; import java.util.Arrays; -public class OrchestrationException extends Exception { +public class OrchestrationException extends RuntimeException { public OrchestrationException(Throwable cause) { super(cause); diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApi.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApi.java index fd115702588..47ba6dedd84 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApi.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApi.java @@ -17,13 +17,20 @@ public interface ClusterApi { String clusterInfo(); ClusterId clusterId(); ServiceType serviceType(); + + /** Some human-readable string naming the service(s) to a human reader. */ + String serviceDescription(boolean plural); + boolean isStorageCluster(); - /** Returns the reasons no services are up in the implied group, or empty if some services are up. */ - Optional<SuspensionReasons> reasonsForNoServicesInGroupIsUp(); + boolean isConfigServerLike(); + + /** Returns the non-empty reasons for why all services are down, or otherwise empty. */ + Optional<SuspensionReasons> allServicesDown(); + boolean noServicesOutsideGroupIsDown() throws HostStateChangeDeniedException; - int percentageOfServicesDown(); + int percentageOfServicesDownOutsideGroup(); int percentageOfServicesDownIfGroupIsAllowedToBeDown(); Optional<StorageNode> storageNodeInGroup(); diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApiImpl.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApiImpl.java index 00cb65a09b0..6880700e4a9 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApiImpl.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApiImpl.java @@ -19,7 +19,6 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Collections; -import java.util.Comparator; import java.util.HashSet; import java.util.Map; import java.util.Optional; @@ -41,9 +40,10 @@ class ClusterApiImpl implements ClusterApi { private final ClusterControllerClientFactory clusterControllerClientFactory; private final Clock clock; private final Set<ServiceInstance> servicesInGroup; - private final Set<ServiceInstance> servicesDownInGroup; private final Set<ServiceInstance> servicesNotInGroup; - private final Set<ServiceInstance> servicesDownAndNotInGroup; + + /** Lazily initialized in servicesDownAndNotInGroup(), do not access directly. */ + private Set<ServiceInstance> servicesDownAndNotInGroup = null; /* * There are two sources for the number of config servers in a cluster. The config server config and the node @@ -81,9 +81,6 @@ class ClusterApiImpl implements ClusterApi { servicesInGroup = serviceInstancesByLocality.getOrDefault(true, Collections.emptySet()); servicesNotInGroup = serviceInstancesByLocality.getOrDefault(false, Collections.emptySet()); - servicesDownInGroup = servicesInGroup.stream().filter(this::serviceEffectivelyDown).collect(Collectors.toSet()); - servicesDownAndNotInGroup = servicesNotInGroup.stream().filter(this::serviceEffectivelyDown).collect(Collectors.toSet()); - int serviceInstances = serviceCluster.serviceInstances().size(); if (clusterParams.size().isPresent() && serviceInstances < clusterParams.size().getAsInt()) { missingServices = clusterParams.size().getAsInt() - serviceInstances; @@ -110,6 +107,11 @@ class ClusterApiImpl implements ClusterApi { } @Override + public String serviceDescription(boolean plural) { + return serviceCluster.serviceDescription(plural); + } + + @Override public boolean isStorageCluster() { return VespaModelUtil.isStorage(serviceCluster); } @@ -120,7 +122,12 @@ class ClusterApiImpl implements ClusterApi { } @Override - public Optional<SuspensionReasons> reasonsForNoServicesInGroupIsUp() { + public boolean isConfigServerLike() { + return serviceCluster.isConfigServerLike(); + } + + @Override + public Optional<SuspensionReasons> allServicesDown() { SuspensionReasons reasons = new SuspensionReasons(); for (ServiceInstance service : servicesInGroup) { @@ -156,29 +163,18 @@ class ClusterApiImpl implements ClusterApi { @Override public boolean noServicesOutsideGroupIsDown() throws HostStateChangeDeniedException { - Optional<ServiceInstance> serviceWithUnknownStatus = servicesNotInGroup - .stream() - .filter(serviceInstance -> serviceInstance.serviceStatus() == ServiceStatus.UNKNOWN) - .min(Comparator.comparing(ServiceInstance::descriptiveName)); - if (serviceWithUnknownStatus.isPresent()) { - throw new HostStateChangeDeniedException( - nodeGroup, - HostedVespaPolicy.UNKNOWN_SERVICE_STATUS, - "Service status of " + serviceWithUnknownStatus.get().descriptiveName() + " is not yet known"); - } - - return servicesDownAndNotInGroup.size() + missingServices == 0; + return servicesDownAndNotInGroup().size() + missingServices == 0; } @Override - public int percentageOfServicesDown() { - int numberOfServicesDown = servicesDownAndNotInGroup.size() + missingServices + servicesDownInGroup.size(); + public int percentageOfServicesDownOutsideGroup() { + int numberOfServicesDown = servicesDownAndNotInGroup().size() + missingServices; return numberOfServicesDown * 100 / (serviceCluster.serviceInstances().size() + missingServices); } @Override public int percentageOfServicesDownIfGroupIsAllowedToBeDown() { - int numberOfServicesDown = servicesDownAndNotInGroup.size() + missingServices + servicesInGroup.size(); + int numberOfServicesDown = servicesDownAndNotInGroup().size() + missingServices + servicesInGroup.size(); return numberOfServicesDown * 100 / (serviceCluster.serviceInstances().size() + missingServices); } @@ -200,15 +196,14 @@ class ClusterApiImpl implements ClusterApi { description.append(" "); final int nodeLimit = 3; - description.append("Suspended hosts: "); description.append(suspended.stream().sorted().distinct().limit(nodeLimit).collect(Collectors.toList()).toString()); if (suspended.size() > nodeLimit) { - description.append(", and " + (suspended.size() - nodeLimit) + " more"); + description.append(" and " + (suspended.size() - nodeLimit) + " others"); } - description.append("."); + description.append(" are suspended."); } - Set<ServiceInstance> downElsewhere = servicesDownAndNotInGroup.stream() + Set<ServiceInstance> downElsewhere = servicesDownAndNotInGroup().stream() .filter(serviceInstance -> !suspended.contains(serviceInstance.hostName())) .collect(Collectors.toSet()); @@ -217,7 +212,6 @@ class ClusterApiImpl implements ClusterApi { description.append(" "); final int serviceLimit = 2; // services info is verbose - description.append("Services down on resumed hosts: "); description.append(Stream.concat( downElsewhere.stream().map(ServiceInstance::toString).sorted(), missingServices > 0 ? Stream.of(descriptionOfMissingServices) : Stream.of()) @@ -226,9 +220,9 @@ class ClusterApiImpl implements ClusterApi { .toString()); if (downElsewhereTotal > serviceLimit) { - description.append(", and " + (downElsewhereTotal - serviceLimit) + " more"); + description.append(" and " + (downElsewhereTotal - serviceLimit) + " others"); } - description.append("."); + description.append(" are down."); } return description.toString(); @@ -288,19 +282,31 @@ class ClusterApiImpl implements ClusterApi { return "{ clusterId=" + clusterId() + ", serviceType=" + serviceType() + " }"; } + private Set<ServiceInstance> servicesDownAndNotInGroup() { + if (servicesDownAndNotInGroup == null) { + servicesDownAndNotInGroup = servicesNotInGroup.stream().filter(this::serviceEffectivelyDown).collect(Collectors.toSet()); + } + return servicesDownAndNotInGroup; + } + private HostStatus hostStatus(HostName hostName) { return hostInfos.getOrNoRemarks(hostName).status(); } - private boolean serviceEffectivelyDown(ServiceInstance service) { + private boolean serviceEffectivelyDown(ServiceInstance service) throws HostStateChangeDeniedException { if (hostStatus(service.hostName()).isSuspended()) { return true; } - if (service.serviceStatus() == ServiceStatus.DOWN) { - return true; + switch (service.serviceStatus()) { + case DOWN: return true; + case UNKNOWN: + throw new HostStateChangeDeniedException( + nodeGroup, + HostedVespaPolicy.UNKNOWN_SERVICE_STATUS, + "Service status of " + service.descriptiveName() + " is not yet known"); + default: + return false; } - - return false; } } diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/HostedVespaClusterPolicy.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/HostedVespaClusterPolicy.java index 208e12690ff..fe06ef7d75b 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/HostedVespaClusterPolicy.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/HostedVespaClusterPolicy.java @@ -23,6 +23,16 @@ public class HostedVespaClusterPolicy implements ClusterPolicy { @Override public SuspensionReasons verifyGroupGoingDownIsFine(ClusterApi clusterApi) throws HostStateChangeDeniedException { + return verifyGroupGoingDownIsFine(clusterApi, false); + } + + @Override + public void verifyGroupGoingDownPermanentlyIsFine(ClusterApi clusterApi) throws HostStateChangeDeniedException { + verifyGroupGoingDownIsFine(clusterApi, true); + } + + private SuspensionReasons verifyGroupGoingDownIsFine(ClusterApi clusterApi, boolean permanent) + throws HostStateChangeDeniedException { if (clusterApi.noServicesOutsideGroupIsDown()) { return SuspensionReasons.nothingNoteworthy(); } @@ -32,47 +42,28 @@ public class HostedVespaClusterPolicy implements ClusterPolicy { return SuspensionReasons.nothingNoteworthy(); } - Optional<SuspensionReasons> suspensionReasons = clusterApi.reasonsForNoServicesInGroupIsUp(); - if (suspensionReasons.isPresent()) { - return suspensionReasons.get(); + // Be a bit more cautious when removing nodes permanently + if (!permanent) { + // Disallow suspending a 2nd and downed config server to avoid losing ZK quorum. + if (!clusterApi.isConfigServerLike()) { + Optional<SuspensionReasons> suspensionReasons = clusterApi.allServicesDown(); + if (suspensionReasons.isPresent()) { + return suspensionReasons.get(); + } + } } String message = percentageOfServicesAllowedToBeDown <= 0 - ? "Suspension of service with type '" + clusterApi.serviceType() + "' not allowed: " - + clusterApi.percentageOfServicesDown() + "% are suspended already." + clusterApi.downDescription() - : "Suspension of service with type '" + clusterApi.serviceType() - + "' would increase from " + clusterApi.percentageOfServicesDown() - + "% to " + clusterApi.percentageOfServicesDownIfGroupIsAllowedToBeDown() - + "%, over the limit of " + percentageOfServicesAllowedToBeDown + "%." - + clusterApi.downDescription(); + ? clusterApi.percentageOfServicesDownOutsideGroup() + "% of the " + clusterApi.serviceDescription(true) + + " are down or suspended already:" + clusterApi.downDescription() + : "The percentage of downed or suspended " + clusterApi.serviceDescription(true) + + " would increase from " + clusterApi.percentageOfServicesDownOutsideGroup() + "% to " + + clusterApi.percentageOfServicesDownIfGroupIsAllowedToBeDown() + "% (limit is " + + percentageOfServicesAllowedToBeDown + "%):" + clusterApi.downDescription(); throw new HostStateChangeDeniedException(clusterApi.getNodeGroup(), ENOUGH_SERVICES_UP_CONSTRAINT, message); } - @Override - public void verifyGroupGoingDownPermanentlyIsFine(ClusterApi clusterApi) throws HostStateChangeDeniedException { - // This policy is similar to verifyGroupGoingDownIsFine, except that having no services up in the group will - // not allow the suspension: We are a bit more cautious when removing nodes. - - if (clusterApi.noServicesOutsideGroupIsDown()) { - return; - } - - int percentageOfServicesAllowedToBeDown = getConcurrentSuspensionLimit(clusterApi) - .asPercentage(); - if (clusterApi.percentageOfServicesDownIfGroupIsAllowedToBeDown() <= percentageOfServicesAllowedToBeDown) { - return; - } - - throw new HostStateChangeDeniedException( - clusterApi.getNodeGroup(), - ENOUGH_SERVICES_UP_CONSTRAINT, - "Down percentage for service type " + clusterApi.serviceType() - + " would increase to " + clusterApi.percentageOfServicesDownIfGroupIsAllowedToBeDown() - + "%, over the limit of " + percentageOfServicesAllowedToBeDown + "%." - + clusterApi.downDescription()); - } - // Non-private for testing purposes ConcurrentSuspensionLimitForCluster getConcurrentSuspensionLimit(ClusterApi clusterApi) { // Possible service clusters on a node as of 2021-01-22: diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/OrchestratorTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/OrchestratorTest.java index 83c902ed981..f885d5301de 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/OrchestratorTest.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/OrchestratorTest.java @@ -102,7 +102,7 @@ public class OrchestratorTest { fail(); } catch (HostStateChangeDeniedException e) { assertThat(e.getMessage(), containsString("Changing the state of cfg2 would violate enough-services-up")); - assertThat(e.getMessage(), containsString("Suspended hosts: [cfg1]")); + assertThat(e.getMessage(), containsString("[cfg1] are suspended.")); } // cfg1 is removed from the application @@ -114,7 +114,7 @@ public class OrchestratorTest { fail(); } catch (HostStateChangeDeniedException e) { assertThat(e.getMessage(), containsString("Changing the state of cfg2 would violate enough-services-up")); - assertThat(e.getMessage(), containsString("Services down on resumed hosts: [1 missing config server]")); + assertThat(e.getMessage(), containsString("[1 missing config server] are down.")); } // cfg1 is reprovisioned, added to the node repo, and activated @@ -129,7 +129,7 @@ public class OrchestratorTest { fail(); } catch (HostStateChangeDeniedException e) { assertThat(e.getMessage(), containsString("Changing the state of cfg1 would violate enough-services-up")); - assertThat(e.getMessage(), containsString("Suspended hosts: [cfg2]")); + assertThat(e.getMessage(), containsString("[cfg2] are suspended")); } // etc (should be the same as for cfg1) diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ClusterApiImplTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ClusterApiImplTest.java index da8591c6631..3ea739b385e 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ClusterApiImplTest.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ClusterApiImplTest.java @@ -95,11 +95,11 @@ public class ClusterApiImplTest { assertEquals("{ clusterId=cluster, serviceType=service-type }", clusterApi.clusterInfo()); assertFalse(clusterApi.isStorageCluster()); - assertEquals(" Suspended hosts: [host3, host4]. Services down on resumed hosts: [" + - "ServiceInstance{configId=service-2, hostName=host2, serviceStatus=" + - "ServiceStatusInfo{status=DOWN, since=Optional.empty, lastChecked=Optional.empty}}].", + assertEquals(" [host3, host4] are suspended. [ServiceInstance{configId=service-2, hostName=host2, " + + "serviceStatus=ServiceStatusInfo{status=DOWN, since=Optional.empty, lastChecked=Optional.empty}}] " + + "are down.", clusterApi.downDescription()); - assertEquals(60, clusterApi.percentageOfServicesDown()); + assertEquals(60, clusterApi.percentageOfServicesDownOutsideGroup()); assertEquals(80, clusterApi.percentageOfServicesDownIfGroupIsAllowedToBeDown()); } @@ -181,8 +181,8 @@ public class ClusterApiImplTest { fail(); } catch (HostStateChangeDeniedException e) { assertThat(e.getMessage(), - containsString("Suspension of service with type 'configserver' not allowed: 33% are suspended already. " + - "Services down on resumed hosts: [1 missing config server].")); + containsString("Changing the state of cfg1 would violate enough-services-up: 33% of the config " + + "servers are down or suspended already: [1 missing config server] are down.")); } } @@ -201,18 +201,25 @@ public class ClusterApiImplTest { fail(); } catch (HostStateChangeDeniedException e) { assertThat(e.getMessage(), - containsString("Suspension of service with type 'hostadmin' not allowed: 33% are suspended already. " + - "Services down on resumed hosts: [1 missing config server host].")); + containsString("Changing the state of cfg1 would violate enough-services-up: 33% of the config " + + "server hosts are down or suspended already: [1 missing config server host] are down.")); } } @Test - public void testCfg1SuspendsIfDownWithMissingCfg3() throws HostStateChangeDeniedException { + public void testCfg1DoesNotSuspendIfDownWithMissingCfg3() throws HostStateChangeDeniedException { ClusterApiImpl clusterApi = makeCfg1ClusterApi(ServiceStatus.DOWN, ServiceStatus.UP); HostedVespaClusterPolicy policy = new HostedVespaClusterPolicy(flagSource, zone); - policy.verifyGroupGoingDownIsFine(clusterApi); + try { + policy.verifyGroupGoingDownIsFine(clusterApi); + fail(); + } catch (HostStateChangeDeniedException e) { + assertThat(e.getMessage(), + containsString("Changing the state of cfg1 would violate enough-services-up: 33% of the config " + + "servers are down or suspended already: [1 missing config server] are down.")); + } } @Test @@ -320,7 +327,7 @@ public class ClusterApiImplTest { clock); assertEquals(expectedNoServicesInGroupIsUp.map(SuspensionReasons::getMessagesInOrder), - clusterApi.reasonsForNoServicesInGroupIsUp().map(SuspensionReasons::getMessagesInOrder)); + clusterApi.allServicesDown().map(SuspensionReasons::getMessagesInOrder)); assertEquals(expectedNoServicesOutsideGroupIsDown, clusterApi.noServicesOutsideGroupIsDown()); } diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/policy/HostedVespaClusterPolicyTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/policy/HostedVespaClusterPolicyTest.java index 303dabebba8..8f47051621f 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/policy/HostedVespaClusterPolicyTest.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/policy/HostedVespaClusterPolicyTest.java @@ -131,13 +131,14 @@ public class HostedVespaClusterPolicyTest { int percentageOfServicesDownIfGroupIsAllowedToBeDown, boolean expectSuccess) throws HostStateChangeDeniedException { when(clusterApi.noServicesOutsideGroupIsDown()).thenReturn(noServicesOutsideGroupIsDown); - when(clusterApi.reasonsForNoServicesInGroupIsUp()).thenReturn(noServicesInGroupIsUp); + when(clusterApi.allServicesDown()).thenReturn(noServicesInGroupIsUp); when(clusterApi.percentageOfServicesDownIfGroupIsAllowedToBeDown()).thenReturn(20); doReturn(ConcurrentSuspensionLimitForCluster.TEN_PERCENT).when(policy).getConcurrentSuspensionLimit(clusterApi); when(applicationApi.applicationId()).thenReturn(ApplicationId.fromSerializedForm("a:b:c")); when(clusterApi.serviceType()).thenReturn(new ServiceType("service-type")); - when(clusterApi.percentageOfServicesDown()).thenReturn(5); + when(clusterApi.serviceDescription(true)).thenReturn("services of {service-type,cluster-id}"); + when(clusterApi.percentageOfServicesDownOutsideGroup()).thenReturn(5); when(clusterApi.percentageOfServicesDownIfGroupIsAllowedToBeDown()).thenReturn(percentageOfServicesDownIfGroupIsAllowedToBeDown); when(clusterApi.downDescription()).thenReturn(" Down description"); @@ -152,9 +153,10 @@ public class HostedVespaClusterPolicyTest { } } catch (HostStateChangeDeniedException e) { if (!expectSuccess) { - assertEquals("Changing the state of node-group would violate enough-services-up: " + - "Suspension of service with type 'service-type' would increase from 5% to 13%, " + - "over the limit of 10%. Down description", e.getMessage()); + assertEquals("Changing the state of node-group would violate enough-services-up: The percentage of downed " + + "or suspended services of {service-type,cluster-id} would increase from 5% to 13% (limit is 10%): " + + "Down description", + e.getMessage()); assertEquals("enough-services-up", e.getConstraintName()); } } diff --git a/parent/pom.xml b/parent/pom.xml index ca61f6707e7..fa04c167d0e 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -543,18 +543,6 @@ <version>${prometheus.client.version}</version> </dependency> <dependency> - <!-- TODO: Try to remove, as this overlaps with javax.activation. --> - <groupId>jakarta.activation</groupId> - <artifactId>jakarta.activation-api</artifactId> - <version>1.2.1</version> - </dependency> - <dependency> - <!-- TODO: Try to remove, as this conflicts with javax.xml.bind:jaxb-api --> - <groupId>jakarta.xml.bind</groupId> - <artifactId>jakarta.xml.bind-api</artifactId> - <version>2.3.2</version> - </dependency> - <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> diff --git a/searchlib/abi-spec.json b/searchlib/abi-spec.json index e8e3a3cb133..2468fd0c5c7 100644 --- a/searchlib/abi-spec.json +++ b/searchlib/abi-spec.json @@ -1034,6 +1034,7 @@ "public static final int LDEXP", "public static final int POW", "public static final int BIT", + "public static final int HAMMING", "public static final int MAP", "public static final int REDUCE", "public static final int JOIN", @@ -1389,7 +1390,8 @@ "public static final enum com.yahoo.searchlib.rankingexpression.rule.Function max", "public static final enum com.yahoo.searchlib.rankingexpression.rule.Function min", "public static final enum com.yahoo.searchlib.rankingexpression.rule.Function pow", - "public static final enum com.yahoo.searchlib.rankingexpression.rule.Function bit" + "public static final enum com.yahoo.searchlib.rankingexpression.rule.Function bit", + "public static final enum com.yahoo.searchlib.rankingexpression.rule.Function hamming" ] }, "com.yahoo.searchlib.rankingexpression.rule.FunctionNode": { diff --git a/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/evaluation/TensorValue.java b/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/evaluation/TensorValue.java index e41732f9d16..33ba3c6ef4b 100644 --- a/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/evaluation/TensorValue.java +++ b/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/evaluation/TensorValue.java @@ -157,6 +157,7 @@ public class TensorValue extends Value { case fmod: return value.fmod(argument); case ldexp: return value.ldexp(argument); case bit: return value.bit(argument); + case hamming: return value.hamming(argument); default: throw new UnsupportedOperationException("Cannot combine two tensors using " + function); } } diff --git a/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/rule/Function.java b/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/rule/Function.java index 16aa947986d..3958711f74b 100644 --- a/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/rule/Function.java +++ b/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/rule/Function.java @@ -46,7 +46,8 @@ public enum Function implements Serializable { max(2) { public double evaluate(double x, double y) { return max(x,y); } }, min(2) { public double evaluate(double x, double y) { return min(x,y); } }, pow(2) { public double evaluate(double x, double y) { return pow(x,y); } }, - bit(2) { public double evaluate(double x, double y) { return ((int)y < 8 && (int)y >= 0 && ((int)x & (1 << (int)y)) != 0) ? 1.0 : 0.0; } }; + bit(2) { public double evaluate(double x, double y) { return ((int)y < 8 && (int)y >= 0 && ((int)x & (1 << (int)y)) != 0) ? 1.0 : 0.0; } }, + hamming(2) { public double evaluate(double x, double y) { return ScalarFunctions.Hamming.hamming(x, y); } }; private final int arity; diff --git a/searchlib/src/main/javacc/RankingExpressionParser.jj b/searchlib/src/main/javacc/RankingExpressionParser.jj index 99eff010628..7bfbfd6c005 100755 --- a/searchlib/src/main/javacc/RankingExpressionParser.jj +++ b/searchlib/src/main/javacc/RankingExpressionParser.jj @@ -124,6 +124,7 @@ TOKEN : // MIN <POW: "pow"> | <BIT: "bit"> | + <HAMMING: "hamming"> | <MAP: "map"> | <REDUCE: "reduce"> | @@ -735,7 +736,8 @@ Function binaryFunctionName() : { } <MAX> { return Function.max; } | <MIN> { return Function.min; } | <POW> { return Function.pow; } | - <BIT> { return Function.bit; } + <BIT> { return Function.bit; } | + <HAMMING> { return Function.hamming; } } List<ExpressionNode> expressionList() : diff --git a/searchlib/src/test/java/com/yahoo/searchlib/rankingexpression/evaluation/EvaluationTestCase.java b/searchlib/src/test/java/com/yahoo/searchlib/rankingexpression/evaluation/EvaluationTestCase.java index 4a3c4b248be..246dbcb2b1e 100644 --- a/searchlib/src/test/java/com/yahoo/searchlib/rankingexpression/evaluation/EvaluationTestCase.java +++ b/searchlib/src/test/java/com/yahoo/searchlib/rankingexpression/evaluation/EvaluationTestCase.java @@ -288,6 +288,8 @@ public class EvaluationTestCase { tester.assertEvaluates("{ {h:0}:1.5, {h:1}:1.5 }", "0.5 + tensor0", "{ {h:0}:1.0,{h:1}:1.0 }"); tester.assertEvaluates("{ {x:0,y:0}:0, {x:1,y:0}:0 }", "atan2(tensor0, tensor1)", "{ {x:0}:0, {x:1}:0 }", "{ {y:0}:1 }"); + tester.assertEvaluates("{ {x:0,y:0}:2, {x:1,y:0}:7 }", + "hamming(tensor0, tensor1)", "{ {x:0}:97, {x:1}:-1 }", "{ {y:0}:1 }"); tester.assertEvaluates("{ {x:0,y:0}:0, {x:1,y:0}:1 }", "tensor0 > tensor1", "{ {x:0}:3, {x:1}:7 }", "{ {y:0}:5 }"); tester.assertEvaluates("{ {x:0,y:0}:1, {x:1,y:0}:0 }", diff --git a/searchlib/src/tests/rankingexpression/rankingexpressionlist b/searchlib/src/tests/rankingexpression/rankingexpressionlist index 77b2294c668..d41570732d9 100644 --- a/searchlib/src/tests/rankingexpression/rankingexpressionlist +++ b/searchlib/src/tests/rankingexpression/rankingexpressionlist @@ -87,6 +87,7 @@ floor(10) relu(10) sigmoid(10) atan2(10, 20); atan2(10,20) +hamming(42, -16); hamming(42,-16) ldexp(10, 20); ldexp(10,20) pow(10, 20); pow(10,20) fmod(10, 20); fmod(10,20) diff --git a/searchlib/src/vespa/searchlib/engine/search_protocol_proto.h b/searchlib/src/vespa/searchlib/engine/search_protocol_proto.h index e2eeca74e44..bb9887f6e74 100644 --- a/searchlib/src/vespa/searchlib/engine/search_protocol_proto.h +++ b/searchlib/src/vespa/searchlib/engine/search_protocol_proto.h @@ -7,6 +7,6 @@ #pragma GCC diagnostic ignored "-Wsuggest-override" #endif -#include "search_protocol.pb.h" +#include <vespa/searchlib/engine/search_protocol.pb.h> #pragma GCC diagnostic pop diff --git a/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java b/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java index 215dc311af3..dbabf1274af 100644 --- a/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java +++ b/security-utils/src/main/java/com/yahoo/security/X509CertificateUtils.java @@ -21,10 +21,13 @@ import java.io.UncheckedIOException; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Duration; @@ -178,4 +181,16 @@ public class X509CertificateUtils { .build(); return new X509CertificateWithKey(cert, keyPair.getPrivate()); } + + /** + * @return certificate SHA-1 fingerprint + */ + public static byte[] getX509CertificateFingerPrint(X509Certificate certificate) { + try { + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + return sha1.digest(certificate.getEncoded()); + } catch (CertificateEncodingException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } } diff --git a/slobrok/src/apps/slobrok/slobrok.cpp b/slobrok/src/apps/slobrok/slobrok.cpp index b2748762a12..18a401e1db6 100644 --- a/slobrok/src/apps/slobrok/slobrok.cpp +++ b/slobrok/src/apps/slobrok/slobrok.cpp @@ -50,7 +50,6 @@ App::Main() { uint32_t portnum = 2773; vespalib::string cfgId; - bool useNewLogic = false; int argi = 1; const char* optArg; @@ -64,7 +63,7 @@ App::Main() portnum = atoi(optArg); break; case 'N': - useNewLogic = true; + // ignored break; default: LOG(error, "unknown option letter '%c'", c); @@ -76,11 +75,11 @@ App::Main() if (cfgId.empty()) { LOG(debug, "no config id specified"); ConfigShim shim(portnum); - mainobj = std::make_unique<SBEnv>(shim, useNewLogic); + mainobj = std::make_unique<SBEnv>(shim); } else { ConfigShim shim(portnum, cfgId); shim.enableStateServer(true); - mainobj = std::make_unique<SBEnv>(shim, useNewLogic); + mainobj = std::make_unique<SBEnv>(shim); } hook_sigterm(); res = mainobj->MainLoop(); diff --git a/slobrok/src/vespa/slobrok/server/CMakeLists.txt b/slobrok/src/vespa/slobrok/server/CMakeLists.txt index ae1a05c5181..5168758e46d 100644 --- a/slobrok/src/vespa/slobrok/server/CMakeLists.txt +++ b/slobrok/src/vespa/slobrok/server/CMakeLists.txt @@ -1,7 +1,6 @@ # Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. vespa_add_library(slobrok_slobrokserver SOURCES - cmd.cpp configshim.cpp exchange_manager.cpp i_monitored_server.cpp @@ -23,8 +22,6 @@ vespa_add_library(slobrok_slobrokserver request_completion_handler.cpp reserved_name.cpp rpc_mapping_monitor.cpp - rpc_server_manager.cpp - rpc_server_map.cpp rpchooks.cpp rpcmirror.cpp sbenv.cpp diff --git a/slobrok/src/vespa/slobrok/server/cmd.cpp b/slobrok/src/vespa/slobrok/server/cmd.cpp deleted file mode 100644 index df856189d89..00000000000 --- a/slobrok/src/vespa/slobrok/server/cmd.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - - -#include "cmd.h" -#include "rpc_server_map.h" -#include "reserved_name.h" -#include "remote_slobrok.h" -#include "sbenv.h" - -#include <vespa/log/log.h> -LOG_SETUP(".slobrok.server.cmd"); - -namespace slobrok { - -//----------------------------------------------------------------------------- - -struct ScriptData { - SBEnv &env; - const std::string name; - const std::string spec; - FRT_RPCRequest * const registerRequest; - - enum { - RDC_INIT, XCH_WANTADD, CHK_RPCSRV, XCH_DOADD, XCH_IGNORE, RDC_INVAL - } _state; - - ScriptData(SBEnv &e, const std::string &n, const std::string &s, FRT_RPCRequest *r) - : env(e), name(n), spec(s), registerRequest(r), _state(RDC_INIT) - {} -}; - -//----------------------------------------------------------------------------- - -const std::string & -ScriptCommand::name() { return _data->name; } - -const std::string & -ScriptCommand::spec() { return _data->spec; } - -ScriptCommand::ScriptCommand(std::unique_ptr<ScriptData> data) - : _data(std::move(data)) -{} - -ScriptCommand::ScriptCommand(ScriptCommand &&) = default; -ScriptCommand& -ScriptCommand::operator= (ScriptCommand &&) = default; -ScriptCommand::~ScriptCommand() = default; - -ScriptCommand -ScriptCommand::makeRegRpcSrvCmd(SBEnv &env, - const std::string &name, const std::string &spec, - FRT_RPCRequest *req) -{ - return ScriptCommand(std::make_unique<ScriptData>(env, name, spec, req)); -} - -ScriptCommand -ScriptCommand::makeIgnoreCmd(SBEnv &env, const std::string & name, const std::string &spec) -{ - auto data = std::make_unique<ScriptData>(env, name, spec, nullptr); - data->_state = ScriptData::XCH_IGNORE; - return ScriptCommand(std::move(data)); -} - -ScriptCommand -ScriptCommand::makeRegCompleter(SBEnv &env, - const std::string &name, const std::string &spec, - FRT_RPCRequest *req) -{ - auto data = std::make_unique<ScriptData>(env, name, spec, req); - data->_state = ScriptData::XCH_DOADD; - return ScriptCommand(std::move(data)); -} - -void -ScriptCommand::doRequest() -{ - LOG_ASSERT(_data->_state == ScriptData::RDC_INIT); - doneHandler(OkState()); -} - -void cleanupReservation(ScriptData & data) -{ - RpcServerMap &map = data.env.rpcServerMap(); - const ReservedName *rsvp = map.getReservation(data.name.c_str()); - if (rsvp != nullptr && rsvp->isLocal) { - map.removeReservation(data.name.c_str()); - } -} - -void -ScriptCommand::doneHandler(OkState result) -{ - LOG_ASSERT(_data); - std::unique_ptr<ScriptData> dataUP = std::move(_data); - LOG_ASSERT(! _data); - ScriptData & data = *dataUP; - const char *name_p = data.name.c_str(); - const char *spec_p = data.spec.c_str(); - ExchangeManager &xch = data.env.exchangeManager(); - RpcServerManager &rsm = data.env.rpcServerManager(); - - if (result.failed()) { - LOG(warning, "failed [%s->%s] in state %d: %s", name_p, spec_p, data._state, result.errorMsg.c_str()); - if (data._state != ScriptData::XCH_IGNORE) { - cleanupReservation(data); - } - // XXX should handle different state errors differently? - if (data.registerRequest != nullptr) { - data.registerRequest->SetError(FRTE_RPC_METHOD_FAILED, result.errorMsg.c_str()); - data.registerRequest->Return(); - } else { - LOG(warning, "ignored: %s", result.errorMsg.c_str()); - } - return; - } - if (data._state == ScriptData::RDC_INIT) { - LOG(spam, "phase wantAdd(%s,%s)", name_p, spec_p); - data._state = ScriptData::XCH_WANTADD; - xch.wantAdd(std::move(dataUP)); - return; - } else if (data._state == ScriptData::XCH_WANTADD) { - LOG(spam, "phase addManaged(%s,%s)", name_p, spec_p); - data._state = ScriptData::CHK_RPCSRV; - rsm.addManaged(std::move(dataUP)); - return; - } else if (data._state == ScriptData::CHK_RPCSRV) { - LOG(spam, "phase doAdd(%s,%s)", name_p, spec_p); - data._state = ScriptData::XCH_DOADD; - xch.doAdd(std::move(dataUP)); - return; - } else if (data._state == ScriptData::XCH_DOADD) { - LOG(debug, "done doAdd(%s,%s)", name_p, spec_p); - data._state = ScriptData::RDC_INVAL; - // all OK - if (data.registerRequest != nullptr) { - data.registerRequest->Return(); - } - cleanupReservation(data); - return; - } else if (data._state == ScriptData::XCH_IGNORE) { - return; - } - // no other state should be possible - LOG_ABORT("should not be reached"); -} - -//----------------------------------------------------------------------------- - -} // namespace slobrok diff --git a/slobrok/src/vespa/slobrok/server/cmd.h b/slobrok/src/vespa/slobrok/server/cmd.h deleted file mode 100644 index e7f42f75e42..00000000000 --- a/slobrok/src/vespa/slobrok/server/cmd.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#pragma once - -#include "ok_state.h" -#include <memory> - -class FRT_RPCRequest; - -namespace slobrok { - -class SBEnv; -struct ScriptData; - -class ScriptCommand -{ -private: - std::unique_ptr<ScriptData> _data; - ScriptCommand(std::unique_ptr<ScriptData> data); -public: - const std::string &name(); - const std::string &spec(); - - ScriptCommand(ScriptCommand &&); - ScriptCommand& operator= (ScriptCommand &&); - ~ScriptCommand(); - - static ScriptCommand makeRegRpcSrvCmd(SBEnv &env, const std::string &name, const std::string &spec, FRT_RPCRequest *req); - static ScriptCommand makeIgnoreCmd(SBEnv &env, const std::string &name, const std::string &spec); - static ScriptCommand makeRegCompleter(SBEnv &env, const std::string &name, const std::string &spec, FRT_RPCRequest *req); - - void doneHandler(OkState result); - void doRequest(); -}; - -} // namespace slobrok diff --git a/slobrok/src/vespa/slobrok/server/exchange_manager.cpp b/slobrok/src/vespa/slobrok/server/exchange_manager.cpp index 94e951ca252..89167842c1b 100644 --- a/slobrok/src/vespa/slobrok/server/exchange_manager.cpp +++ b/slobrok/src/vespa/slobrok/server/exchange_manager.cpp @@ -1,7 +1,6 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "exchange_manager.h" -#include "rpc_server_map.h" #include "sbenv.h" #include <vespa/fnet/frt/supervisor.h> #include <vespa/vespalib/util/overload.h> @@ -14,11 +13,9 @@ namespace slobrok { //----------------------------------------------------------------------------- -ExchangeManager::ExchangeManager(SBEnv &env, RpcServerMap &rpcsrvmap) +ExchangeManager::ExchangeManager(SBEnv &env) : _partners(), - _env(env), - _rpcsrvmanager(env.rpcServerManager()), - _rpcsrvmap(rpcsrvmap) + _env(env) { } @@ -69,35 +66,13 @@ ExchangeManager::getPartnerList() void ExchangeManager::forwardRemove(const std::string & name, const std::string & spec) { - WorkPackage *package = new WorkPackage(WorkPackage::OP_REMOVE, *this, - ScriptCommand::makeIgnoreCmd(_env, name, spec)); + WorkPackage *package = new WorkPackage(WorkPackage::OP_REMOVE, ServiceMapping{name, spec}, *this); for (const auto & entry : _partners) { package->addItem(entry.second.get()); } package->expedite(); } -void -ExchangeManager::doAdd(ScriptCommand rdc) -{ - WorkPackage *package = new WorkPackage(WorkPackage::OP_DOADD, *this, std::move(rdc)); - - for (const auto & entry : _partners) { - package->addItem(entry.second.get()); - } - package->expedite(); -} - - -void -ExchangeManager::wantAdd(ScriptCommand rdc) -{ - WorkPackage *package = new WorkPackage(WorkPackage::OP_WANTADD, *this, std::move(rdc)); - for (const auto & entry : _partners) { - package->addItem(entry.second.get()); - } - package->expedite(); -} RemoteSlobrok * ExchangeManager::lookupPartner(const std::string & name) const { @@ -128,22 +103,8 @@ void ExchangeManager::healthCheck() { auto newWorldList = env().consensusMap().currentConsensus(); - if (! _env.useNewLogic()) { - auto oldWorldServices = env().rpcServerMap().allManaged(); - ServiceMappingList oldWorldList; - for (const auto *nsp : oldWorldServices) { - oldWorldList.emplace_back(nsp->getName(), nsp->getSpec()); - } - std::sort(oldWorldList.begin(), oldWorldList.end()); - vespalib::string diff = diffLists(oldWorldList, newWorldList); - if (! diff.empty()) { - LOG(warning, "Diff from old world rpcServerMap to new world consensus map: %s", - diff.c_str()); - } - } for (const auto & [ name, partner ] : _partners) { partner->maybeStartFetch(); - partner->maybePushMine(); auto remoteList = partner->remoteMap().allMappings(); // 0 is expected (when remote is down) if (remoteList.size() != 0) { @@ -205,14 +166,12 @@ ExchangeManager::WorkPackage::WorkItem::~WorkItem() } -ExchangeManager::WorkPackage::WorkPackage(op_type op, - ExchangeManager &exchanger, - ScriptCommand script) +ExchangeManager::WorkPackage::WorkPackage(op_type op, const ServiceMapping &mapping, ExchangeManager &exchanger) : _work(), _doneCnt(0), _numDenied(0), - _script(std::move(script)), _exchanger(exchanger), + _mapping(mapping), _optype(op) { } @@ -230,9 +189,9 @@ ExchangeManager::WorkPackage::doneItem(bool denied) (int)_doneCnt, (int)_work.size(), (int)_numDenied); if (_doneCnt == _work.size()) { if (_numDenied > 0) { - _script.doneHandler(OkState(_numDenied, "denied by remote")); - } else { - _script.doneHandler(OkState()); + LOG(debug, "work package [%s->%s]: %zd/%zd denied by remote", + _mapping.name.c_str(), _mapping.spec.c_str(), + _numDenied, _doneCnt); } delete this; } @@ -245,18 +204,12 @@ ExchangeManager::WorkPackage::addItem(RemoteSlobrok *partner) if (! partner->isConnected()) { return; } - const char *name_p = _script.name().c_str(); - const char *spec_p = _script.spec().c_str(); + const char *name_p = _mapping.name.c_str(); + const char *spec_p = _mapping.spec.c_str(); FRT_RPCRequest *r = _exchanger._env.getSupervisor()->AllocRPCRequest(); - // XXX should recheck rpcsrvmap again - if (_optype == OP_REMOVE) { - r->SetMethodName("slobrok.internal.doRemove"); - } else if (_optype == OP_WANTADD) { - r->SetMethodName("slobrok.internal.wantAdd"); - } else if (_optype == OP_DOADD) { - r->SetMethodName("slobrok.internal.doAdd"); - } + LOG_ASSERT(_optype == OP_REMOVE); + r->SetMethodName("slobrok.internal.doRemove"); r->GetParams()->AddString(_exchanger._env.mySpec().c_str()); r->GetParams()->AddString(name_p); r->GetParams()->AddString(spec_p); @@ -274,7 +227,6 @@ ExchangeManager::WorkPackage::expedite() size_t sz = _work.size(); if (sz == 0) { // no remotes need doing. - _script.doneHandler(OkState()); delete this; return; } diff --git a/slobrok/src/vespa/slobrok/server/exchange_manager.h b/slobrok/src/vespa/slobrok/server/exchange_manager.h index 18a20274a75..6891686c94d 100644 --- a/slobrok/src/vespa/slobrok/server/exchange_manager.h +++ b/slobrok/src/vespa/slobrok/server/exchange_manager.h @@ -2,7 +2,6 @@ #pragma once #include "ok_state.h" -#include "cmd.h" #include "remote_slobrok.h" #include <deque> @@ -14,8 +13,6 @@ namespace slobrok { //----------------------------------------------------------------------------- class SBEnv; -class RpcServerMap; -class RpcServerManager; //----------------------------------------------------------------------------- @@ -61,37 +58,31 @@ private: std::vector<std::unique_ptr<WorkItem>> _work; size_t _doneCnt; size_t _numDenied; - ScriptCommand _script; public: ExchangeManager &_exchanger; - enum op_type { OP_NOP, OP_WANTADD, OP_DOADD, OP_REMOVE }; - op_type _optype; + enum op_type { OP_REMOVE }; + const ServiceMapping _mapping; + const op_type _optype; void addItem(RemoteSlobrok *partner); void doneItem(bool denied); void expedite(); WorkPackage(const WorkPackage&) = delete; WorkPackage& operator= (const WorkPackage&) = delete; - WorkPackage(op_type op, - ExchangeManager &exchanger, - ScriptCommand donehandler); + WorkPackage(op_type op, const ServiceMapping &mapping, ExchangeManager &exchanger); ~WorkPackage(); }; SBEnv &_env; - RpcServerManager &_rpcsrvmanager; - RpcServerMap &_rpcsrvmap; vespalib::string diffLists(const ServiceMappingList &lhs, const ServiceMappingList &rhs); public: ExchangeManager(const ExchangeManager &) = delete; ExchangeManager &operator=(const ExchangeManager &) = delete; - ExchangeManager(SBEnv &env, RpcServerMap &rpcsrvmap); + ExchangeManager(SBEnv &env); ~ExchangeManager(); SBEnv &env() { return _env; } - RpcServerManager &rpcServerManager() { return _rpcsrvmanager; } - RpcServerMap &rpcServerMap() { return _rpcsrvmap; } OkState addPartner(const std::string & spec); void removePartner(const std::string & spec); @@ -99,9 +90,6 @@ public: void forwardRemove(const std::string & name, const std::string & spec); - void wantAdd(ScriptCommand rdc); - void doAdd(ScriptCommand rdc); - RemoteSlobrok *lookupPartner(const std::string & name) const; void healthCheck(); }; diff --git a/slobrok/src/vespa/slobrok/server/local_rpc_monitor_map.h b/slobrok/src/vespa/slobrok/server/local_rpc_monitor_map.h index 96c0ee03245..173a0455e43 100644 --- a/slobrok/src/vespa/slobrok/server/local_rpc_monitor_map.h +++ b/slobrok/src/vespa/slobrok/server/local_rpc_monitor_map.h @@ -1,8 +1,6 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #pragma once -#include "cmd.h" -#include "managed_rpc_server.h" #include "map_listener.h" #include "map_source.h" #include "mapping_monitor.h" @@ -12,6 +10,8 @@ #include "service_map_history.h" #include "service_mapping.h" +#include <vespa/fnet/task.h> + #include <vector> #include <memory> #include <map> diff --git a/slobrok/src/vespa/slobrok/server/remote_check.cpp b/slobrok/src/vespa/slobrok/server/remote_check.cpp index 157b959dbfe..da4d1ebc3dd 100644 --- a/slobrok/src/vespa/slobrok/server/remote_check.cpp +++ b/slobrok/src/vespa/slobrok/server/remote_check.cpp @@ -2,8 +2,6 @@ #include "remote_check.h" #include "named_service.h" -#include "rpc_server_map.h" -#include "rpc_server_manager.h" #include "remote_slobrok.h" #include "random.h" #include "exchange_manager.h" @@ -13,12 +11,9 @@ LOG_SETUP(".slobrok.server.remote_check"); namespace slobrok { -RemoteCheck::RemoteCheck(FNET_Scheduler *sched, - RpcServerMap& rpcsrvmap, - RpcServerManager& rpcsrvman, - ExchangeManager& exch) +RemoteCheck::RemoteCheck(FNET_Scheduler *sched, ExchangeManager& exch) : FNET_Task(sched), - _rpcsrvmap(rpcsrvmap), _rpcsrvmanager(rpcsrvman), _exchanger(exch) + _exchanger(exch) { double seconds = randomIn(15.3, 27.9); Schedule(seconds); diff --git a/slobrok/src/vespa/slobrok/server/remote_check.h b/slobrok/src/vespa/slobrok/server/remote_check.h index e0cf89c177d..11eb85401fe 100644 --- a/slobrok/src/vespa/slobrok/server/remote_check.h +++ b/slobrok/src/vespa/slobrok/server/remote_check.h @@ -20,17 +20,12 @@ class ExchangeManager; class RemoteCheck : public FNET_Task { private: - RpcServerMap &_rpcsrvmap; - RpcServerManager &_rpcsrvmanager; ExchangeManager &_exchanger; RemoteCheck(const RemoteCheck &); // Not used RemoteCheck &operator=(const RemoteCheck &); // Not used public: - explicit RemoteCheck(FNET_Scheduler *sched, - RpcServerMap& rpcsrvmap, - RpcServerManager& rpcsrvman, - ExchangeManager& exchanger); + explicit RemoteCheck(FNET_Scheduler *sched, ExchangeManager& exchanger); ~RemoteCheck(); private: void PerformTask() override; diff --git a/slobrok/src/vespa/slobrok/server/remote_slobrok.cpp b/slobrok/src/vespa/slobrok/server/remote_slobrok.cpp index d867d955dac..4b308e90e37 100644 --- a/slobrok/src/vespa/slobrok/server/remote_slobrok.cpp +++ b/slobrok/src/vespa/slobrok/server/remote_slobrok.cpp @@ -1,8 +1,6 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "remote_slobrok.h" -#include "rpc_server_map.h" -#include "rpc_server_manager.h" #include "exchange_manager.h" #include "sbenv.h" #include <vespa/fnet/frt/supervisor.h> @@ -18,7 +16,6 @@ namespace slobrok { RemoteSlobrok::RemoteSlobrok(const std::string &name, const std::string &spec, ExchangeManager &manager) : _exchanger(manager), - _rpcsrvmanager(manager.rpcServerManager()), _remote(nullptr), _serviceMapMirror(), _rpcserver(name, spec, *this), @@ -26,11 +23,7 @@ RemoteSlobrok::RemoteSlobrok(const std::string &name, const std::string &spec, _failCnt(0), _consensusSubscription(MapSubscription::subscribe(_serviceMapMirror, _exchanger.env().consensusMap())), _remAddPeerReq(nullptr), - _remListReq(nullptr), - _remAddReq(nullptr), - _remRemReq(nullptr), - _remFetchReq(nullptr), - _pending() + _remFetchReq(nullptr) { _rpcserver.healthCheck(); } @@ -38,8 +31,6 @@ RemoteSlobrok::RemoteSlobrok(const std::string &name, const std::string &spec, void RemoteSlobrok::shutdown() { _reconnecter.disable(); - _pending.clear(); - if (_remote != nullptr) { _remote->SubRef(); _remote = nullptr; @@ -51,15 +42,6 @@ void RemoteSlobrok::shutdown() { if (_remAddPeerReq != nullptr) { _remAddPeerReq->Abort(); } - if (_remListReq != nullptr) { - _remListReq->Abort(); - } - if (_remAddReq != nullptr) { - _remAddReq->Abort(); - } - if (_remRemReq != nullptr) { - _remRemReq->Abort(); - } _serviceMapMirror.clear(); } @@ -68,39 +50,6 @@ RemoteSlobrok::~RemoteSlobrok() { // _rpcserver destructor called automatically } -void -RemoteSlobrok::doPending() -{ - if (_remAddReq != nullptr) return; - if (_remRemReq != nullptr) return; - - if (_remote == nullptr) return; - - if ( ! _pending.empty() ) { - std::unique_ptr<NamedService> todo = std::move(_pending.front()); - _pending.pop_front(); - - const NamedService *rpcsrv = _exchanger.rpcServerMap().lookup(todo->getName()); - - if (rpcsrv == nullptr) { - _remRemReq = getSupervisor()->AllocRPCRequest(); - _remRemReq->SetMethodName("slobrok.internal.doRemove"); - _remRemReq->GetParams()->AddString(_exchanger.env().mySpec().c_str()); - _remRemReq->GetParams()->AddString(todo->getName().c_str()); - _remRemReq->GetParams()->AddString(todo->getSpec().c_str()); - _remote->InvokeAsync(_remRemReq, 2.0, this); - } else { - _remAddReq = getSupervisor()->AllocRPCRequest(); - _remAddReq->SetMethodName("slobrok.internal.doAdd"); - _remAddReq->GetParams()->AddString(_exchanger.env().mySpec().c_str()); - _remAddReq->GetParams()->AddString(todo->getName().c_str()); - _remAddReq->GetParams()->AddString(rpcsrv->getSpec().c_str()); - _remote->InvokeAsync(_remAddReq, 2.0, this); - } - // XXX should save this and pick up on RequestDone() - } -} - void RemoteSlobrok::maybeStartFetch() { if (_remFetchReq != nullptr) return; if (_remote == nullptr) return; @@ -168,21 +117,6 @@ void RemoteSlobrok::handleFetchResult() { } } - - -void -RemoteSlobrok::pushMine() -{ - // all mine - std::vector<const NamedService *> mine = _exchanger.rpcServerMap().allManaged(); - while (mine.size() > 0) { - const NamedService *now = mine.back(); - mine.pop_back(); - _pending.push_back(std::make_unique<NamedService>(now->getName(), now->getSpec())); - } - doPending(); -} - void RemoteSlobrok::RequestDone(FRT_RPCRequest *req) { @@ -190,7 +124,6 @@ RemoteSlobrok::RequestDone(FRT_RPCRequest *req) handleFetchResult(); return; } - FRT_Values &answer = *(req->GetReturn()); if (req == _remAddPeerReq) { // handle response after asking remote slobrok to add me as a peer: if (req->IsError()) { @@ -201,96 +134,15 @@ RemoteSlobrok::RequestDone(FRT_RPCRequest *req) myname, myspec, getName().c_str(), getSpec().c_str(), req->GetErrorMessage()); req->SubRef(); _remAddPeerReq = nullptr; - goto retrylater; + fail(); + return; } req->SubRef(); _remAddPeerReq = nullptr; - // next step is to ask the remote to send its list of managed names: - LOG_ASSERT(_remListReq == nullptr); - _remListReq = getSupervisor()->AllocRPCRequest(); - _remListReq->SetMethodName("slobrok.internal.listManagedRpcServers"); - if (_remote != nullptr) { - _remote->InvokeAsync(_remListReq, 3.0, this); - } - // when _remListReq is returned, our managed list is added - } else if (req == _remListReq) { - // handle the list sent from the remote: - if (req->IsError() - || strcmp(answer.GetTypeString(), "SS") != 0) - { - LOG(error, "error listing remote slobrok %s at %s: %s", - getName().c_str(), getSpec().c_str(), req->GetErrorMessage()); - req->SubRef(); - _remListReq = nullptr; - goto retrylater; - } - uint32_t numNames = answer.GetValue(0)._string_array._len; - uint32_t numSpecs = answer.GetValue(1)._string_array._len; - - if (numNames != numSpecs) { - LOG(error, "inconsistent array lengths from %s at %s", getName().c_str(), getSpec().c_str()); - req->SubRef(); - _remListReq = nullptr; - goto retrylater; - } - FRT_StringValue *names = answer.GetValue(0)._string_array._pt; - FRT_StringValue *specs = answer.GetValue(1)._string_array._pt; - - for (uint32_t idx = 0; idx < numNames; idx++) { - _rpcsrvmanager.addRemote(names[idx]._str, specs[idx]._str); - } - req->SubRef(); - _remListReq = nullptr; - - // next step is to push the ones I own: - maybeStartFetch(); - maybePushMine(); - } else if (req == _remAddReq) { - // handle response after pushing some name that we managed: - if (req->IsError() && (req->GetErrorCode() == FRTE_RPC_CONNECTION || - req->GetErrorCode() == FRTE_RPC_TIMEOUT)) - { - LOG(error, "connection error adding to remote slobrok: %s", req->GetErrorMessage()); - req->SubRef(); - _remAddReq = nullptr; - goto retrylater; - } - if (req->IsError()) { - FRT_Values &args = *req->GetParams(); - const char *rpcsrvname = args[1]._string._str; - const char *rpcsrvspec = args[2]._string._str; - LOG(warning, "error adding [%s -> %s] to remote slobrok: %s", - rpcsrvname, rpcsrvspec, req->GetErrorMessage()); - _rpcsrvmanager.removeLocal(rpcsrvname, rpcsrvspec); - } - req->SubRef(); - _remAddReq = nullptr; - doPending(); - } else if (req == _remRemReq) { - // handle response after pushing some remove we had pending: - if (req->IsError() && (req->GetErrorCode() == FRTE_RPC_CONNECTION || - req->GetErrorCode() == FRTE_RPC_TIMEOUT)) - { - LOG(error, "connection error adding to remote slobrok: %s", req->GetErrorMessage()); - req->SubRef(); - _remRemReq = nullptr; - goto retrylater; - } - if (req->IsError()) { - LOG(warning, "error removing on remote slobrok: %s", req->GetErrorMessage()); - } - req->SubRef(); - _remRemReq = nullptr; - doPending(); } else { LOG(error, "got unknown request back in RequestDone()"); LOG_ASSERT(req == nullptr); } - - return; - retrylater: - fail(); - return; } @@ -321,25 +173,6 @@ RemoteSlobrok::fail() _reconnecter.scheduleTryConnect(); } - -void -RemoteSlobrok::maybePushMine() -{ - if (_remote != nullptr && - _remAddPeerReq == nullptr && - _remListReq == nullptr && - _remAddReq == nullptr && - _remRemReq == nullptr) - { - LOG(debug, "spamming remote at %s with my names", getName().c_str()); - pushMine(); - } else { - LOG(debug, "not pushing mine, as we have: remote %p r.a.p.r=%p r.l.r=%p r.a.r=%p r.r.r=%p", - _remote, _remAddPeerReq, _remListReq, _remAddReq, _remRemReq); - } -} - - void RemoteSlobrok::notifyOkRpcSrv(ManagedRpcServer *rpcsrv) { @@ -357,10 +190,7 @@ RemoteSlobrok::notifyOkRpcSrv(ManagedRpcServer *rpcsrv) _remote = getSupervisor()->GetTarget(getSpec().c_str()); maybeStartFetch(); - // at this point, we will do (in sequence): - // ask peer to connect to us too; - // ask peer for its list of managed rpcservers, adding to our database - // add our managed rpcserver on peer + // at this point, we will ask peer to connect to us too; // any failure will cause disconnect and retry. _remAddPeerReq = getSupervisor()->AllocRPCRequest(); @@ -368,7 +198,6 @@ RemoteSlobrok::notifyOkRpcSrv(ManagedRpcServer *rpcsrv) _remAddPeerReq->GetParams()->AddString(_exchanger.env().mySpec().c_str()); _remAddPeerReq->GetParams()->AddString(_exchanger.env().mySpec().c_str()); _remote->InvokeAsync(_remAddPeerReq, 3.0, this); - // when _remAddPeerReq is returned, our managed list is added via doAdd() } void diff --git a/slobrok/src/vespa/slobrok/server/remote_slobrok.h b/slobrok/src/vespa/slobrok/server/remote_slobrok.h index b980aa90de0..ef7f39c08ed 100644 --- a/slobrok/src/vespa/slobrok/server/remote_slobrok.h +++ b/slobrok/src/vespa/slobrok/server/remote_slobrok.h @@ -2,7 +2,6 @@ #pragma once #include "ok_state.h" -#include "cmd.h" #include "i_rpc_server_manager.h" #include "managed_rpc_server.h" #include "service_map_mirror.h" @@ -12,7 +11,6 @@ namespace slobrok { //----------------------------------------------------------------------------- -class RpcServerManager; class ExchangeManager; //----------------------------------------------------------------------------- @@ -43,7 +41,6 @@ private: }; ExchangeManager &_exchanger; - RpcServerManager &_rpcsrvmanager; FRT_Target *_remote; ServiceMapMirror _serviceMapMirror; ManagedRpcServer _rpcserver; @@ -53,14 +50,8 @@ private: std::unique_ptr<MapSubscription> _consensusSubscription; FRT_RPCRequest *_remAddPeerReq; - FRT_RPCRequest *_remListReq; - FRT_RPCRequest *_remAddReq; - FRT_RPCRequest *_remRemReq; FRT_RPCRequest *_remFetchReq; - std::deque<std::unique_ptr<NamedService>> _pending; - void pushMine(); - void doPending(); void handleFetchResult(); public: @@ -72,7 +63,6 @@ public: void fail(); bool isConnected() const { return (_remote != nullptr); } void tryConnect(); - void maybePushMine(); void maybeStartFetch(); void invokeAsync(FRT_RPCRequest *req, double timeout, FRT_IRequestWait *rwaiter); const std::string & getName() const { return _rpcserver.getName(); } diff --git a/slobrok/src/vespa/slobrok/server/rpc_server_manager.cpp b/slobrok/src/vespa/slobrok/server/rpc_server_manager.cpp deleted file mode 100644 index a9a748323f7..00000000000 --- a/slobrok/src/vespa/slobrok/server/rpc_server_manager.cpp +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#include "rpc_server_manager.h" -#include "reserved_name.h" -#include "rpc_server_map.h" -#include "remote_slobrok.h" -#include "sbenv.h" -#include <vespa/vespalib/util/stringfmt.h> -#include <sstream> - -#include <vespa/log/log.h> -LOG_SETUP(".slobrok.server.rpc_server_manager"); - -using vespalib::make_string_short::fmt; - -namespace slobrok { - -RpcServerManager::RpcServerManager(SBEnv &sbenv) - : FNET_Task(sbenv.getScheduler()), - _rpcsrvmap(sbenv.rpcServerMap()), - _exchanger(sbenv.exchangeManager()), - _env(sbenv), - _addManageds(), - _deleteList() -{ -} - -static OkState -validateName(const std::string & rpcsrvname) -{ - const char *p = rpcsrvname.c_str(); - while (*p != '\0') { - // important: disallow '*' - if (strchr("+,-./:=@[]_{}~<>" - "0123456789" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz", *p) == nullptr) - { - std::ostringstream tmp; - tmp << "Illegal character '" << *p << "' ("; - tmp << (int)(*p)<< ") in rpcserver name"; - return OkState(13, tmp.str().c_str()); - } - ++p; - } - if (p == rpcsrvname) { - return OkState(13, "empty rpcserver name"); - } - return OkState(); -} - - -OkState -RpcServerManager::checkPartner(const std::string & remslobrok) -{ - if (remslobrok == _env.mySpec()) { - return OkState(13, "remote slobrok using my rpcserver name"); - } - const RemoteSlobrok *partner = _exchanger.lookupPartner(remslobrok); - if (partner == nullptr) { - return OkState(13, "remote slobrok not a partner"); - } - return OkState(); -} - -OkState -RpcServerManager::addRemReservation(const std::string & remslobrok, const std::string & name, const std::string &spec) -{ - OkState state = checkPartner(remslobrok); - if (state.failed()) return state; - - OkState valid = validateName(name); - if (valid.failed()) return valid; - - const NamedService *old = _rpcsrvmap.lookupManaged(name); - if (old != nullptr) { - if (old->getSpec() == spec) { - // was alright already - return OkState(0, "already registered"); - } - LOG(warning, "remote %s tried to register [%s -> %s] but we already have [%s -> %s] registered!", - remslobrok.c_str(), name.c_str(), spec.c_str(), old->getName().c_str(), old->getSpec().c_str()); - return OkState(FRTE_RPC_METHOD_FAILED, "already managed by me"); - } - if (_rpcsrvmap.conflictingReservation(name, spec)) { - return OkState(FRTE_RPC_METHOD_FAILED, "registration for name already in progress"); - } - _rpcsrvmap.addReservation(std::make_unique<ReservedName>(name, spec, false)); - return OkState(0, "done"); -} - - -OkState -RpcServerManager::addMyReservation(const std::string & name, const std::string & spec) -{ - OkState valid = validateName(name); - if (valid.failed()) return valid; - - const NamedService *old = _rpcsrvmap.lookupManaged(name); - if (old != nullptr) { - if (old->getSpec() == spec) { - // was alright already - return OkState(0, "already registered"); - } else { - return OkState(FRTE_RPC_METHOD_FAILED, fmt("name %s registered (to %s), cannot register %s", - name.c_str(), old->getSpec().c_str(), spec.c_str())); - } - } - - // check if we already are in the progress of adding this - if (_rpcsrvmap.conflictingReservation(name, spec)) { - const ReservedName * rsv = _rpcsrvmap.getReservation(name); - LOG(warning, "conflicting registrations: wanted [%s -> %s] but [%s -> %s] already reserved", - name.c_str(), spec.c_str(), rsv->getName().c_str(), rsv->getSpec().c_str()); - return OkState(FRTE_RPC_METHOD_FAILED, - "registration for name already in progress with a different spec"); - } - _rpcsrvmap.removeReservation(name); - _rpcsrvmap.addReservation(std::make_unique<ReservedName>(name, spec, true)); - return OkState(0, "done"); -} - - -OkState -RpcServerManager::addRemote(const std::string & name, const std::string &spec) -{ - OkState valid = validateName(name); - if (valid.failed()) return valid; - - if (alreadyManaged(name, spec)) { - return OkState(0, "already correct"); - } - const NamedService *old = _rpcsrvmap.lookup(name); - if (old != nullptr) { - if (old->getSpec() != spec) { - LOG(warning, "collision on remote add: name %s registered to %s locally, " - "but another location broker wants it registered to %s", - name.c_str(), old->getSpec().c_str(), spec.c_str()); - removeRemote(name, old->getSpec()); - return OkState(13, "registered, with different spec"); - } - // was alright already, remove reservation - _rpcsrvmap.removeReservation(name); - return OkState(0, "already correct"); - } - _rpcsrvmap.removeReservation(name); - auto rpcsrv = std::make_unique<ManagedRpcServer>(name, spec, *this); - _rpcsrvmap.addNew(std::move(rpcsrv)); - return OkState(0, "done"); -} - -OkState -RpcServerManager::remove(ManagedRpcServer *rpcsrv) -{ - const NamedService *td = _rpcsrvmap.lookup(rpcsrv->getName()); - if (td == rpcsrv) { - return removeLocal(rpcsrv->getName(), rpcsrv->getSpec()); - } else { - return OkState(1, "not currently registered"); - } -} - - -OkState -RpcServerManager::removeRemote(const std::string &name, const std::string &spec) -{ - const NamedService *old = _rpcsrvmap.lookup(name); - if (old == nullptr) { - // was alright already, remove any reservation too - _rpcsrvmap.removeReservation(name); - return OkState(0, "already done"); - } - if (old->getSpec() != spec) { - return OkState(1, "name registered, but with different spec"); - } - std::unique_ptr<NamedService> td = _rpcsrvmap.remove(name); - LOG_ASSERT(td.get() == old); - return OkState(0, "done"); -} - -OkState -RpcServerManager::removeLocal(const std::string & name, const std::string &spec) -{ - const NamedService *td = _rpcsrvmap.lookup(name); - if (td == nullptr) { - // already removed, nop - return OkState(); - } - - const RemoteSlobrok *partner = _exchanger.lookupPartner(name); - if (partner != nullptr) { - return OkState(13, "cannot unregister partner slobrok"); - } - - const ManagedRpcServer *rpcsrv = _rpcsrvmap.lookupManaged(name); - if (rpcsrv == nullptr) { - return OkState(13, "not a local rpcserver"); - } - - if (rpcsrv->getSpec() != spec) { - // the client can probably ignore this "error" - // or log it on level INFO? - return OkState(1, fmt("name registered, but with different spec (%s)", rpcsrv->getSpec().c_str())); - } - auto tdUP = _rpcsrvmap.remove(name); - LOG_ASSERT(tdUP.get() == rpcsrv); - _exchanger.forwardRemove(name, spec); - return OkState(); -} - - -void -RpcServerManager::addManaged(ScriptCommand rdc) -{ - const std::string &name = rdc.name(); - const std::string &spec = rdc.spec(); - auto newRpcServer = std::make_unique<ManagedRpcServer>(name, spec, *this); - ManagedRpcServer & rpcsrv = *newRpcServer; - _rpcsrvmap.addNew(std::move(newRpcServer)); - for (size_t i = 0; i < _addManageds.size(); i++) { - if (_addManageds[i].rpcsrv == nullptr) { - _addManageds[i].rpcsrv = &rpcsrv; - _addManageds[i].handler = std::move(rdc); - rpcsrv.healthCheck(); - return; - } - } - _addManageds.emplace_back(&rpcsrv, std::move(rdc)); - rpcsrv.healthCheck(); - return; -} - - - -bool -RpcServerManager::alreadyManaged(const std::string &name, const std::string &spec) -{ - const ManagedRpcServer *rpcsrv = _rpcsrvmap.lookupManaged(name); - if (rpcsrv != nullptr) { - if (rpcsrv->getSpec() == spec) { - return true; - } - } - return false; -} - - -RpcServerManager::~RpcServerManager() -{ - Kill(); - PerformTask(); -} - - -void -RpcServerManager::PerformTask() -{ - std::vector<std::unique_ptr<NamedService>> deleteAfterSwap; - std::swap(deleteAfterSwap, _deleteList); -} - - -void -RpcServerManager::notifyFailedRpcSrv(ManagedRpcServer *rpcsrv, std::string errmsg) -{ - _env.countFailedHeartbeat(); - const auto &name = rpcsrv->getName(); - const auto &spec = rpcsrv->getSpec(); - const char *namep = name.c_str(); - const char *specp = spec.c_str(); - std::unique_ptr<NamedService> toDelete; - const NamedService *old = _rpcsrvmap.lookup(rpcsrv->getName()); - if (old == rpcsrv) { - toDelete = _rpcsrvmap.remove(name); - LOG_ASSERT(toDelete.get() == rpcsrv); - LOG(info, "managed server %s at %s failed: %s", namep, specp, errmsg.c_str()); - } else { - // only managed servers should exist, this is bad: - LOG(error, "unmanaged server %s at %s failed: %s", namep, specp, errmsg.c_str()); - } - _exchanger.forwardRemove(name, spec); - for (size_t i = 0; i < _addManageds.size(); ++i) { - if (_addManageds[i].rpcsrv == rpcsrv) { - LOG(warning, "rpcserver %s at %s failed while trying to register", namep, specp); - _addManageds[i].rpcsrv = nullptr; - _addManageds[i].handler.doneHandler(OkState(13, "failed check using listNames callback")); - } - } - if (toDelete) { - _deleteList.push_back(std::move(toDelete)); - ScheduleNow(); - } -} - -void -RpcServerManager::notifyOkRpcSrv(ManagedRpcServer *rpcsrv) -{ - for (size_t i = 0; i < _addManageds.size(); ++i) { - if (_addManageds[i].rpcsrv == rpcsrv) { - _addManageds[i].handler.doneHandler(OkState()); - _addManageds[i].rpcsrv = 0; - } - } - // XXX check if pending wantAdd / doAdd / registerRpcServer -} - -FRT_Supervisor * -RpcServerManager::getSupervisor() -{ - return _env.getSupervisor(); -} - -//----------------------------------------------------------------------------- - -} // namespace slobrok diff --git a/slobrok/src/vespa/slobrok/server/rpc_server_manager.h b/slobrok/src/vespa/slobrok/server/rpc_server_manager.h deleted file mode 100644 index 15b674388e3..00000000000 --- a/slobrok/src/vespa/slobrok/server/rpc_server_manager.h +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#pragma once - -#include "ok_state.h" -#include "cmd.h" -#include "i_rpc_server_manager.h" -#include "named_service.h" -#include <vespa/fnet/task.h> -#include <vector> -#include <memory> - -namespace slobrok { - -class NamedService; -class ManagedRpcServer; -class RemoteSlobrok; -class ReservedName; -class RpcServerMap; -class ExchangeManager; -class SBEnv; - -/** - * @class RpcServerManager - * @brief Main "business logic" for the service location broker. - * - * Used by all external and some internal operations. - * This class actually implements operations, - * checking for validity, manipulating internal datastructures, - * and initiating synchronization operations to peer slobroks. - **/ -class RpcServerManager : public FNET_Task, - public IRpcServerManager -{ -private: - RpcServerMap &_rpcsrvmap; - ExchangeManager &_exchanger; - SBEnv &_env; - - struct MRSandRRSC { - ManagedRpcServer *rpcsrv; - ScriptCommand handler; - MRSandRRSC(ManagedRpcServer *d, ScriptCommand h) - : rpcsrv(d), handler(std::move(h)) {} - }; - std::vector<MRSandRRSC> _addManageds; - std::vector<std::unique_ptr<NamedService>> _deleteList; -public: - OkState checkPartner(const std::string & remslobrok); - - OkState addRemote(const std::string & name, const std::string & spec); - - OkState addRemReservation(const std::string & remslobrok, const std::string & name, const std::string & spec); - OkState addMyReservation(const std::string & name, const std::string & spec); - - bool alreadyManaged(const std::string & name, const std::string & spec); - void addManaged(ScriptCommand rdc); - - OkState remove(ManagedRpcServer *rpcsrv); - - OkState removeLocal(const std::string & name, const std::string & spec); - OkState removeRemote(const std::string & name, const std::string & spec); - - RpcServerManager(const RpcServerManager &) = delete; - RpcServerManager &operator=(const RpcServerManager &) = delete; - RpcServerManager(SBEnv &sbenv); - ~RpcServerManager(); - - void PerformTask() override; - void notifyFailedRpcSrv(ManagedRpcServer *rpcsrv, std::string errmsg) override; - void notifyOkRpcSrv(ManagedRpcServer *rpcsrv) override; - FRT_Supervisor *getSupervisor() override; -}; - -//----------------------------------------------------------------------------- - -} // namespace slobrok - diff --git a/slobrok/src/vespa/slobrok/server/rpc_server_map.cpp b/slobrok/src/vespa/slobrok/server/rpc_server_map.cpp deleted file mode 100644 index fcaaf57570c..00000000000 --- a/slobrok/src/vespa/slobrok/server/rpc_server_map.cpp +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#include "rpc_server_map.h" -#include "reserved_name.h" -#include "rpc_server_manager.h" -#include "sbenv.h" - -#include <vespa/log/log.h> -LOG_SETUP(".slobrok.server.rpc_server_map"); - -namespace slobrok { - -//----------------------------------------------------------------------------- - -ManagedRpcServer * -RpcServerMap::lookupManaged(const std::string & name) const { - auto found = _myrpcsrv_map.find(name); - return (found == _myrpcsrv_map.end()) ? nullptr : found->second.get(); -} - -const NamedService * -RpcServerMap::lookup(const std::string & name) const -{ - return lookupManaged(name); -} - -std::unique_ptr<NamedService> -RpcServerMap::remove(const std::string & name) -{ - auto service = std::move(_myrpcsrv_map[name]); - auto spec = service->getSpec(); - _proxy.remove(ServiceMapping{name, spec}); - _myrpcsrv_map.erase(name); - return service; -} - -std::vector<const NamedService *> -RpcServerMap::allManaged() const -{ - std::vector<const NamedService *> retval; - // get list of all names in myrpcsrv_map - for (const auto & entry : _myrpcsrv_map) { - retval.push_back(entry.second.get()); - } - return retval; -} - - -void -RpcServerMap::add(NamedService *rpcsrv) -{ - const std::string &name = rpcsrv->getName(); - - LOG_ASSERT(rpcsrv != nullptr); - LOG_ASSERT(_myrpcsrv_map.find(name) == _myrpcsrv_map.end()); - - removeReservation(name); - _proxy.add(ServiceMapping{name, rpcsrv->getSpec()}); -} - -void -RpcServerMap::addNew(std::unique_ptr<ManagedRpcServer> rpcsrv) -{ - const std::string &name = rpcsrv->getName(); - - auto oldman = std::move(_myrpcsrv_map[name]); - _myrpcsrv_map.erase(name); - - if (oldman) { - const ReservedName *oldres = _reservations[name].get(); - const std::string &spec = rpcsrv->getSpec(); - _proxy.remove(ServiceMapping{name, spec}); - const std::string &oldname = oldman->getName(); - const std::string &oldspec = oldman->getSpec(); - if (spec != oldspec) { - LOG(warning, "internal state problem: adding [%s at %s] but already had [%s at %s]", - name.c_str(), spec.c_str(), oldname.c_str(), oldspec.c_str()); - if (oldres != nullptr) { - const std::string &n = oldres->getName(); - const std::string &s = oldres->getSpec(); - LOG(warning, "old reservation: [%s at %s]", n.c_str(), s.c_str()); - } - } - } - add(rpcsrv.get()); - _myrpcsrv_map[name] = std::move(rpcsrv); -} - - -void -RpcServerMap::addReservation(std::unique_ptr<ReservedName> rpcsrv) -{ - LOG_ASSERT(rpcsrv != nullptr); - LOG_ASSERT(_myrpcsrv_map.find(rpcsrv->getName()) == _myrpcsrv_map.end()); - - // must not be reserved for something else already - // this should have been checked already, so assert - LOG_ASSERT(! conflictingReservation(rpcsrv->getName(), rpcsrv->getSpec())); - auto old = std::move(_reservations[rpcsrv->getName()]); - LOG_ASSERT(!old - || old->getSpec() == rpcsrv->getSpec() - || ! old->stillReserved()); - _reservations[rpcsrv->getName()] = std::move(rpcsrv); -} - - -/** check if there is a (different) registration for this name in progress */ -bool -RpcServerMap::conflictingReservation(const std::string &name, const std::string &spec) -{ - const ReservedName *resv = _reservations[name].get(); - return (resv != nullptr && - resv->stillReserved() && - resv->getSpec() != spec); -} - -const ReservedName * -RpcServerMap::getReservation(const std::string &name) const { - auto found = _reservations.find(name); - return (found == _reservations.end()) ? nullptr : found->second.get(); -} - -RpcServerMap::RpcServerMap() = default; - -RpcServerMap::~RpcServerMap() = default; - -void -RpcServerMap::removeReservation(const std::string & name) -{ - _reservations.erase(name); -} - -} // namespace slobrok diff --git a/slobrok/src/vespa/slobrok/server/rpc_server_map.h b/slobrok/src/vespa/slobrok/server/rpc_server_map.h deleted file mode 100644 index 3d2999069ea..00000000000 --- a/slobrok/src/vespa/slobrok/server/rpc_server_map.h +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#pragma once - -#include "named_service.h" -#include "service_map_history.h" -#include "proxy_map_source.h" - -#include <memory> -#include <string> -#include <unordered_map> - -namespace slobrok { - -class NamedService; -class ManagedRpcServer; -class ReservedName; - -/** - * @class RpcServerMap - * @brief Contains the actual collections of NamedService (and subclasses) - * objects known by this location broker. - * - * Works as a collection of NamedService objects, but actually contains - * three seperate hashmaps. - **/ - -class RpcServerMap -{ -private: - using ManagedRpcServerMap = std::unordered_map<std::string, std::unique_ptr<ManagedRpcServer>>; - using ReservedNameMap = std::unordered_map<std::string, std::unique_ptr<ReservedName>>; - ManagedRpcServerMap _myrpcsrv_map; - ReservedNameMap _reservations; - ProxyMapSource _proxy; - - void add(NamedService *rpcsrv); - -public: - typedef std::vector<const NamedService *> RpcSrvlist; - - MapSource &proxy() { return _proxy; } - - ManagedRpcServer *lookupManaged(const std::string & name) const; - - const NamedService * lookup(const std::string & name) const; - RpcSrvlist allManaged() const; - - void addNew(std::unique_ptr<ManagedRpcServer> rpcsrv); - std::unique_ptr<NamedService> remove(const std::string & name); - - void addReservation(std::unique_ptr<ReservedName>rpcsrv); - bool conflictingReservation(const std::string & name, const std::string &spec); - - const ReservedName *getReservation(const std::string & name) const; - void removeReservation(const std::string & name); - - RpcServerMap(const RpcServerMap &) = delete; - RpcServerMap &operator=(const RpcServerMap &) = delete; - RpcServerMap(); - ~RpcServerMap(); -}; - -//----------------------------------------------------------------------------- - -} // namespace slobrok - diff --git a/slobrok/src/vespa/slobrok/server/rpchooks.cpp b/slobrok/src/vespa/slobrok/server/rpchooks.cpp index 540060210ed..6ce24be9201 100644 --- a/slobrok/src/vespa/slobrok/server/rpchooks.cpp +++ b/slobrok/src/vespa/slobrok/server/rpchooks.cpp @@ -4,8 +4,6 @@ #include "ok_state.h" #include "named_service.h" #include "request_completion_handler.h" -#include "rpc_server_map.h" -#include "rpc_server_manager.h" #include "remote_slobrok.h" #include "sbenv.h" #include "rpcmirror.h" @@ -38,12 +36,31 @@ public: ~MetricsReport() override { Kill(); } }; +bool match(const char *name, const char *pattern) { + LOG_ASSERT(name != nullptr); + LOG_ASSERT(pattern != nullptr); + while (*pattern != '\0') { + if (*name == *pattern) { + ++name; + ++pattern; + } else if (*pattern == '*') { + ++pattern; + while (*name != '/' && *name != '\0') { + ++name; + } + } else { + return false; + } + } + return (*name == *pattern); +} + } // namespace <unnamed> //----------------------------------------------------------------------------- -RPCHooks::RPCHooks(SBEnv &env, RpcServerMap& rpcsrvmap, RpcServerManager& rpcsrvman) - : _env(env), _rpcsrvmap(rpcsrvmap), _rpcsrvmanager(rpcsrvman), +RPCHooks::RPCHooks(SBEnv &env) + : _env(env), _globalHistory(env.globalHistory()), _localHistory(env.localHistory()), _cnts(Metrics::zero()), @@ -65,25 +82,6 @@ void RPCHooks::reportMetrics() { EV_COUNT("other_reqs", _cnts.otherReqs); } -bool RPCHooks::match(const char *name, const char *pattern) { - LOG_ASSERT(name != nullptr); - LOG_ASSERT(pattern != nullptr); - while (*pattern != '\0') { - if (*name == *pattern) { - ++name; - ++pattern; - } else if (*pattern == '*') { - ++pattern; - while (*name != '/' && *name != '\0') { - ++name; - } - } else { - return false; - } - } - return (*name == *pattern); -} - void RPCHooks::initRPC(FRT_Supervisor *supervisor) { _m_reporter = std::make_unique<MetricsReport>(supervisor, *this); @@ -229,11 +227,6 @@ void RPCHooks::initRPC(FRT_Supervisor *supervisor) { //------------------------------------------------------------------------- } - -bool RPCHooks::useNewLogic() const { - return _env.useNewLogic(); -} - void RPCHooks::rpc_listNamesServed(FRT_RPCRequest *req) { FRT_Values &dst = *req->GetReturn(); FRT_StringValue *names = dst.AddStringArray(1); @@ -257,9 +250,6 @@ void RPCHooks::rpc_registerRpcServer(FRT_RPCRequest *req) { } req->Detach(); _env.localMonitorMap().addLocal(mapping, std::make_unique<RequestCompletionHandler>(req)); - // TODO: remove this - auto script = ScriptCommand::makeRegRpcSrvCmd(_env, dName, dSpec, nullptr); - script.doRequest(); return; } diff --git a/slobrok/src/vespa/slobrok/server/rpchooks.h b/slobrok/src/vespa/slobrok/server/rpchooks.h index b68eb9007a8..bd051df64f1 100644 --- a/slobrok/src/vespa/slobrok/server/rpchooks.h +++ b/slobrok/src/vespa/slobrok/server/rpchooks.h @@ -40,8 +40,6 @@ public: private: SBEnv &_env; - RpcServerMap &_rpcsrvmap; - RpcServerManager &_rpcsrvmanager; ServiceMapHistory &_globalHistory; ServiceMapHistory &_localHistory; @@ -49,44 +47,33 @@ private: std::unique_ptr<FNET_Task> _m_reporter; public: - RPCHooks(SBEnv &env, RpcServerMap& rpcsrvmap, RpcServerManager& rpcsrvman); + RPCHooks(SBEnv &env); ~RPCHooks() override; - static bool match(const char *name, const char *pattern); - void initRPC(FRT_Supervisor *supervisor); void reportMetrics(); const Metrics& getMetrics() const { return _cnts; } void countFailedHeartbeat() { _cnts.heartBeatFails++; } private: - bool useNewLogic() const; - - void rpc_lookupRpcServer(FRT_RPCRequest *req); - - void new_registerRpcServer(FRT_RPCRequest *req); - void new_unregisterRpcServer(FRT_RPCRequest *req); - void new_wantAdd(FRT_RPCRequest *req); - void new_doRemove(FRT_RPCRequest *req); - void new_doAdd(FRT_RPCRequest *req); - void rpc_registerRpcServer(FRT_RPCRequest *req); void rpc_unregisterRpcServer(FRT_RPCRequest *req); - void rpc_addPeer(FRT_RPCRequest *req); void rpc_removePeer(FRT_RPCRequest *req); + void rpc_incrementalFetch(FRT_RPCRequest *req); + void rpc_doRemove(FRT_RPCRequest *req); + void rpc_fetchLocalView(FRT_RPCRequest *req); + void rpc_listNamesServed(FRT_RPCRequest *req); + + /** for unit tests and debugging, consider removing some of these: */ + void rpc_lookupRpcServer(FRT_RPCRequest *req); void rpc_listManagedRpcServers(FRT_RPCRequest *req); void rpc_lookupManaged(FRT_RPCRequest *req); void rpc_listAllRpcServers(FRT_RPCRequest *req); - void rpc_incrementalFetch(FRT_RPCRequest *req); void rpc_wantAdd(FRT_RPCRequest *req); void rpc_doAdd(FRT_RPCRequest *req); - void rpc_doRemove(FRT_RPCRequest *req); - void rpc_fetchLocalView(FRT_RPCRequest *req); - - void rpc_listNamesServed(FRT_RPCRequest *req); - void rpc_getRpcServerHistory(FRT_RPCRequest *req); + /** consider removing: */ void rpc_stop(FRT_RPCRequest *req); void rpc_version(FRT_RPCRequest *req); }; diff --git a/slobrok/src/vespa/slobrok/server/sbenv.cpp b/slobrok/src/vespa/slobrok/server/sbenv.cpp index 42debe1556c..1d279994cb0 100644 --- a/slobrok/src/vespa/slobrok/server/sbenv.cpp +++ b/slobrok/src/vespa/slobrok/server/sbenv.cpp @@ -97,19 +97,16 @@ ConfigTask::PerformTask() } // namespace slobrok::<unnamed> -SBEnv::SBEnv(const ConfigShim &shim) : SBEnv(shim, true) {} - -SBEnv::SBEnv(const ConfigShim &shim, bool) +SBEnv::SBEnv(const ConfigShim &shim) : _transport(std::make_unique<FNET_Transport>(TransportConfig().drop_empty_buffers(true))), _supervisor(std::make_unique<FRT_Supervisor>(_transport.get())), _configShim(shim), _configurator(shim.factory().create(*this)), _shuttingDown(false), - _useNewLogic(true), _partnerList(), _me(createSpec(_configShim.portNumber())), - _rpcHooks(*this, _rpcsrvmap, _rpcsrvmanager), - _remotechecktask(std::make_unique<RemoteCheck>(getSupervisor()->GetScheduler(), _rpcsrvmap, _rpcsrvmanager, _exchanger)), + _rpcHooks(*this), + _remotechecktask(std::make_unique<RemoteCheck>(getSupervisor()->GetScheduler(), _exchanger)), _health(), _metrics(_rpcHooks, *_transport), _components(), @@ -117,9 +114,7 @@ SBEnv::SBEnv(const ConfigShim &shim, bool) [this] (MappingMonitorOwner &owner) { return std::make_unique<RpcMappingMonitor>(*_supervisor, owner); }), - _rpcsrvmanager(*this), - _exchanger(*this, _rpcsrvmap), - _rpcsrvmap() + _exchanger(*this) { srandom(time(nullptr) ^ getpid()); // note: feedback loop between these two: @@ -201,7 +196,6 @@ SBEnv::MainLoop() return 0; } - void SBEnv::setup(const std::vector<std::string> &cfg) { @@ -272,7 +266,7 @@ SBEnv::removePeer(const std::string &name, const std::string &spec) if (partner == nullptr) { return OkState(0, "remote slobrok not a partner"); } - _exchanger.removePartner(name); + _exchanger.removePartner(spec); return OkState(0, "done"); } diff --git a/slobrok/src/vespa/slobrok/server/sbenv.h b/slobrok/src/vespa/slobrok/server/sbenv.h index c6fd8905131..09986e037a9 100644 --- a/slobrok/src/vespa/slobrok/server/sbenv.h +++ b/slobrok/src/vespa/slobrok/server/sbenv.h @@ -3,8 +3,6 @@ #include "named_service.h" #include "rpc_mapping_monitor.h" -#include "rpc_server_map.h" -#include "rpc_server_manager.h" #include "remote_slobrok.h" #include "exchange_manager.h" #include "configshim.h" @@ -44,7 +42,6 @@ private: ConfigShim _configShim; Configurator::UP _configurator; bool _shuttingDown; - const bool _useNewLogic; SBEnv(const SBEnv &); // Not used SBEnv &operator=(const SBEnv &); // Not used @@ -62,9 +59,7 @@ private: UnionServiceMap _consensusMap; ServiceMapHistory _globalVisibleHistory; - RpcServerManager _rpcsrvmanager; ExchangeManager _exchanger; - RpcServerMap _rpcsrvmap; std::unique_ptr<MapSubscription> _localMonitorSubscription; std::unique_ptr<MapSubscription> _consensusSubscription; @@ -72,7 +67,6 @@ private: public: explicit SBEnv(const ConfigShim &shim); - SBEnv(const ConfigShim &shim, bool useNewConsensusLogic); ~SBEnv(); FNET_Transport *getTransport() { return _transport.get(); } @@ -83,9 +77,7 @@ public: void suspend(); void resume(); - RpcServerManager& rpcServerManager() { return _rpcsrvmanager; } ExchangeManager& exchangeManager() { return _exchanger; } - RpcServerMap& rpcServerMap() { return _rpcsrvmap; } ServiceMapHistory& globalHistory() { return _globalVisibleHistory; @@ -107,12 +99,11 @@ public: bool isSuspended() const { return false; } bool isShuttingDown() const { return _shuttingDown; } - bool useNewLogic() const { return _useNewLogic; } int MainLoop(); - OkState addPeer(const std::string& name, const std::string &spec); - OkState removePeer(const std::string& name, const std::string &spec); + OkState addPeer(const std::string& name, const std::string& spec); + OkState removePeer(const std::string& name, const std::string& spec); void countFailedHeartbeat() { _rpcHooks.countFailedHeartbeat(); } }; diff --git a/storage/src/vespa/storage/config/distributorconfiguration.h b/storage/src/vespa/storage/config/distributorconfiguration.h index 7aa10893b80..09b30db086a 100644 --- a/storage/src/vespa/storage/config/distributorconfiguration.h +++ b/storage/src/vespa/storage/config/distributorconfiguration.h @@ -1,8 +1,8 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #pragma once -#include "config-stor-distributormanager.h" -#include "config-stor-visitordispatcher.h" +#include <vespa/storage/config/config-stor-distributormanager.h> +#include <vespa/storage/config/config-stor-visitordispatcher.h> #include <vespa/vespalib/stllike/hash_set.h> #include <vespa/storage/common/storagecomponent.h> #include <vespa/vespalib/util/time.h> diff --git a/storage/src/vespa/storage/distributor/distributor_bucket_space.cpp b/storage/src/vespa/storage/distributor/distributor_bucket_space.cpp index 9ec4d31eb32..3cfb8d70b7d 100644 --- a/storage/src/vespa/storage/distributor/distributor_bucket_space.cpp +++ b/storage/src/vespa/storage/distributor/distributor_bucket_space.cpp @@ -50,7 +50,7 @@ DistributorBucketSpace::enumerate_available_nodes() _distribution_bits = _clusterState->getDistributionBitCount(); auto node_count = _clusterState->getNodeCount(lib::NodeType::STORAGE); if (_pending_cluster_state) { - _distribution_bits = std::min(_distribution_bits, _pending_cluster_state->getDistributionBitCount()); + _distribution_bits = std::max(_distribution_bits, _pending_cluster_state->getDistributionBitCount()); node_count = std::min(node_count, _pending_cluster_state->getNodeCount(lib::NodeType::STORAGE)); } std::vector<bool> nodes(node_count); diff --git a/storage/src/vespa/storage/storageserver/rpc/rpc_envelope_proto.h b/storage/src/vespa/storage/storageserver/rpc/rpc_envelope_proto.h index dbc048cd504..7e065ab0739 100644 --- a/storage/src/vespa/storage/storageserver/rpc/rpc_envelope_proto.h +++ b/storage/src/vespa/storage/storageserver/rpc/rpc_envelope_proto.h @@ -7,6 +7,6 @@ #pragma GCC diagnostic ignored "-Wsuggest-override" #endif -#include "rpc_envelope.pb.h" +#include <vespa/storage/storageserver/rpc/rpc_envelope.pb.h> #pragma GCC diagnostic pop diff --git a/storageapi/src/vespa/storageapi/mbusprot/protobuf_includes.h b/storageapi/src/vespa/storageapi/mbusprot/protobuf_includes.h index 289e5dc355c..f5111ae8061 100644 --- a/storageapi/src/vespa/storageapi/mbusprot/protobuf_includes.h +++ b/storageapi/src/vespa/storageapi/mbusprot/protobuf_includes.h @@ -8,9 +8,9 @@ #pragma GCC diagnostic ignored "-Wsuggest-override" #endif -#include "feed.pb.h" -#include "inspect.pb.h" -#include "visiting.pb.h" -#include "maintenance.pb.h" +#include <vespa/storageapi/mbusprot/feed.pb.h> +#include <vespa/storageapi/mbusprot/inspect.pb.h> +#include <vespa/storageapi/mbusprot/visiting.pb.h> +#include <vespa/storageapi/mbusprot/maintenance.pb.h> #pragma GCC diagnostic pop diff --git a/vdslib/src/main/java/com/yahoo/vdslib/state/ClusterState.java b/vdslib/src/main/java/com/yahoo/vdslib/state/ClusterState.java index c536fc0f4cd..8704b11fdfb 100644 --- a/vdslib/src/main/java/com/yahoo/vdslib/state/ClusterState.java +++ b/vdslib/src/main/java/com/yahoo/vdslib/state/ClusterState.java @@ -3,12 +3,13 @@ package com.yahoo.vdslib.state; import com.yahoo.text.StringUtilities; import java.text.ParseException; -import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.StringTokenizer; -import java.util.TreeMap; -import java.util.TreeSet; /** * Be careful about changing this class, as it mirrors the ClusterState in C++. @@ -18,25 +19,183 @@ public class ClusterState implements Cloneable { private static final NodeState DEFAULT_STORAGE_UP_NODE_STATE = new NodeState(NodeType.STORAGE, State.UP); private static final NodeState DEFAULT_DISTRIBUTOR_UP_NODE_STATE = new NodeState(NodeType.DISTRIBUTOR, State.UP); + private static final NodeState DEFAULT_STORAGE_DOWN_NODE_STATE = new NodeState(NodeType.STORAGE, State.DOWN); + private static final NodeState DEFAULT_DISTRIBUTOR_DOWN_NODE_STATE = new NodeState(NodeType.DISTRIBUTOR, State.DOWN); - private int version = 0; - private State state = State.DOWN; - // nodeStates maps each of the non-up nodes that have an index <= the node count for its type. - private Map<Node, NodeState> nodeStates = new TreeMap<>(); + /** + * Maintains a bitset where all non-down nodes have a bit set. All nodes that differs from defaultUp + * and defaultDown are store explicit in a hash map. + */ + private static class Nodes { + private int logicalNodeCount; + private final NodeType type; + private final BitSet upNodes; + private final Map<Integer, NodeState> nodeStates = new HashMap<>(); + Nodes(NodeType type) { + this.type = type; + upNodes = new BitSet(); + } + Nodes(Nodes b) { + logicalNodeCount = b.logicalNodeCount; + type = b.type; + upNodes = (BitSet) b.upNodes.clone(); + b.nodeStates.forEach((key, value) -> nodeStates.put(key, value.clone())); + } + + void updateMaxIndex(int index) { + if (index > logicalNodeCount) { + upNodes.set(logicalNodeCount, index); + logicalNodeCount = index; + } + } + + int getLogicalNodeCount() { return logicalNodeCount; } + + NodeState getNodeState(int index) { + NodeState ns = nodeStates.get(index); + if (ns != null) return ns; + return (index >= getLogicalNodeCount() || ! upNodes.get(index)) + ? new NodeState(type, State.DOWN) + : new NodeState(type, State.UP); + } + + private void validateInput(Node node, NodeState ns) { + ns.verifyValidInSystemState(node.getType()); + if (node.getType() != type) { + throw new IllegalArgumentException("NodeType '" + node.getType() + "' differs from '" + type + "'"); + } + } + + void setNodeState(Node node, NodeState ns) { + validateInput(node, ns); + int index = node.getIndex(); + if (index >= logicalNodeCount) { + logicalNodeCount = index + 1; + } + setNodeStateInternal(index, ns); + } + + void addNodeState(Node node, NodeState ns) { + validateInput(node, ns); + int index = node.getIndex(); + updateMaxIndex(index + 1); + setNodeStateInternal(index, ns); + } + + private static boolean equalsWithDescription(NodeState a, NodeState b) { + // This is due to NodeState.equals considers semantic equality, and description is not part of that. + return a.equals(b) && ((a.getState() != State.DOWN) || a.getDescription().equals(b.getDescription())); + } + + private void setNodeStateInternal(int index, NodeState ns) { + nodeStates.remove(index); + if (ns.getState() == State.DOWN) { + upNodes.clear(index); + if ( ! equalsWithDescription(defaultDown(), ns)) { + nodeStates.put(index, ns); + } + } else { + upNodes.set(index); + if ( ! equalsWithDescription(defaultUp(), ns)) { + nodeStates.put(index, ns); + } + } + } + + boolean similarToImpl(Nodes other, final NodeStateCmp nodeStateCmp) { + // TODO verify behavior of C++ impl against this + if (logicalNodeCount != other.logicalNodeCount) return false; + if (type != other.type) return false; + if ( ! upNodes.equals(other.upNodes)) return false; + for (Integer node : unionNodeSetWith(other.nodeStates.keySet())) { + final NodeState lhs = nodeStates.get(node); + final NodeState rhs = other.nodeStates.get(node); + if (!nodeStateCmp.similar(type, lhs, rhs)) { + return false; + } + } + return true; + } + + private Set<Integer> unionNodeSetWith(final Set<Integer> otherNodes) { + final Set<Integer> unionNodeSet = new HashSet<>(nodeStates.keySet()); + unionNodeSet.addAll(otherNodes); + return unionNodeSet; + } + + @Override + public String toString() { return toString(false); } + + String toString(boolean verbose) { + StringBuilder sb = new StringBuilder(); + + int nodeCount = verbose ? getLogicalNodeCount() : upNodes.length(); + if ( nodeCount > 0 ) { + sb.append(type == NodeType.DISTRIBUTOR ? " distributor:" : " storage:").append(nodeCount); + for (int i = 0; i < nodeCount; i++) { + String nodeState = getNodeState(i).serialize(i, verbose); + if (!nodeState.isEmpty()) { + sb.append(' ').append(nodeState); + } + } + } + return sb.toString(); + } + + @Override + public boolean equals(Object obj) { + if (! (obj instanceof Nodes)) return false; + Nodes b = (Nodes) obj; + if (logicalNodeCount != b.logicalNodeCount) return false; + if (type != b.type) return false; + if (!upNodes.equals(b.upNodes)) return false; + if (!nodeStates.equals(b.nodeStates)) return false; + return true; + } - // TODO: Change to one count for distributor and one for storage, rather than an array - // TODO: RenameFunction, this is not the highest node count but the highest index - private ArrayList<Integer> nodeCount = new ArrayList<>(2); + @Override + public int hashCode() { + return Objects.hash(logicalNodeCount, type, nodeStates, upNodes); + } + private NodeState defaultDown() { + return type == NodeType.STORAGE + ? DEFAULT_STORAGE_DOWN_NODE_STATE + : DEFAULT_DISTRIBUTOR_DOWN_NODE_STATE; + } + private NodeState defaultUp() { + return defaultUpNodeState(type); + } + } + private int version = 0; + private State state = State.DOWN; private String description = ""; private int distributionBits = 16; + private final Nodes distributorNodes; + private final Nodes storageNodes; + public ClusterState(String serialized) throws ParseException { - nodeCount.add(0); - nodeCount.add(0); + distributorNodes = new Nodes(NodeType.DISTRIBUTOR); + storageNodes = new Nodes(NodeType.STORAGE); deserialize(serialized); } + public ClusterState(ClusterState b) { + version = b.version; + state = b.state; + description = b.description; + distributionBits = b.distributionBits; + distributorNodes = new Nodes(b.distributorNodes); + storageNodes = new Nodes(b.storageNodes); + } + + private Nodes getNodes(NodeType type) { + return (type == NodeType.STORAGE) + ? storageNodes + : (type == NodeType.DISTRIBUTOR) ? distributorNodes : null; + } + /** * Parse a given cluster state string into a returned ClusterState instance, wrapping any * parse exceptions in a RuntimeException. @@ -54,20 +213,7 @@ public class ClusterState implements Cloneable { } public ClusterState clone() { - try{ - ClusterState state = (ClusterState) super.clone(); - state.nodeStates = new TreeMap<>(); - for (Map.Entry<Node, NodeState> entry : nodeStates.entrySet()) { - state.nodeStates.put(entry.getKey(), entry.getValue().clone()); - } - state.nodeCount = new ArrayList<>(2); - state.nodeCount.add(nodeCount.get(0)); - state.nodeCount.add(nodeCount.get(1)); - return state; - } catch (CloneNotSupportedException e) { - assert(false); // Should never happen - return null; - } + return new ClusterState(this); } @Override @@ -77,8 +223,8 @@ public class ClusterState implements Cloneable { if (version != other.version || !state.equals(other.state) || distributionBits != other.distributionBits - || !nodeCount.equals(other.nodeCount) - || !nodeStates.equals(other.nodeStates)) + || !distributorNodes.equals(other.distributorNodes) + || !storageNodes.equals(other.storageNodes)) { return false; } @@ -87,7 +233,7 @@ public class ClusterState implements Cloneable { @Override public int hashCode() { - return java.util.Objects.hash(version, state, distributionBits, nodeCount, nodeStates); + return java.util.Objects.hash(version, state, distributionBits, distributorNodes, storageNodes); } @FunctionalInterface @@ -106,7 +252,7 @@ public class ClusterState implements Cloneable { return similarToImpl(other, this::normalizedNodeStateSimilarToIgnoringInitProgress); } - private boolean similarToImpl(final ClusterState other, final NodeStateCmp nodeStateCmp) { + private boolean similarToImpl(ClusterState other, final NodeStateCmp nodeStateCmp) { if (other == this) { return true; // We're definitely similar to ourselves. } @@ -119,23 +265,11 @@ public class ClusterState implements Cloneable { if (!metaInformationSimilarTo(other)) { return false; } - // TODO verify behavior of C++ impl against this - for (Node node : unionNodeSetWith(other.nodeStates.keySet())) { - final NodeState lhs = nodeStates.get(node); - final NodeState rhs = other.nodeStates.get(node); - if (!nodeStateCmp.similar(node.getType(), lhs, rhs)) { - return false; - } - } + if ( !distributorNodes.similarToImpl(other.distributorNodes, nodeStateCmp)) return false; + if ( !storageNodes.similarToImpl(other.storageNodes, nodeStateCmp)) return false; return true; } - private Set<Node> unionNodeSetWith(final Set<Node> otherNodes) { - final Set<Node> unionNodeSet = new TreeSet<>(nodeStates.keySet()); - unionNodeSet.addAll(otherNodes); - return unionNodeSet; - } - private boolean metaInformationSimilarTo(final ClusterState other) { if (version != other.version || !state.equals(other.state)) { return false; @@ -143,7 +277,7 @@ public class ClusterState implements Cloneable { if (distributionBits != other.distributionBits) { return false; } - return nodeCount.equals(other.nodeCount); + return true; } private boolean normalizedNodeStateSimilarTo(final NodeType nodeType, final NodeState lhs, final NodeState rhs) { @@ -178,12 +312,7 @@ public class ClusterState implements Cloneable { void addNodeState() throws ParseException { if (!empty) { NodeState ns = NodeState.deserialize(node.getType(), sb.toString()); - if (!ns.equals(defaultUpNodeState(node.getType()))) { - nodeStates.put(node, ns); - } - if (nodeCount.get(node.getType().ordinal()) <= node.getIndex()) { - nodeCount.set(node.getType().ordinal(), node.getIndex() + 1); - } + getNodes(node.getType()).addNodeState(node, ns); } empty = true; sb = new StringBuilder(); @@ -257,9 +386,7 @@ public class ClusterState implements Cloneable { } catch (Exception e) { throw new ParseException("Illegal node count '" + value + "' in state: " + serialized, 0); } - if (nodeCount > this.nodeCount.get(nodeType.ordinal())) { - this.nodeCount.set(nodeType.ordinal(), nodeCount); - } + getNodes(nodeType).updateMaxIndex(nodeCount); continue; } int dot2 = key.indexOf('.', dot + 1); @@ -269,8 +396,8 @@ public class ClusterState implements Cloneable { } else { node = new Node(nodeType, Integer.valueOf(key.substring(dot + 1, dot2))); } - if (node.getIndex() >= this.nodeCount.get(nodeType.ordinal())) { - throw new ParseException("Cannot index " + nodeType + " node " + node.getIndex() + " of " + this.nodeCount.get(nodeType.ordinal()) + " in state: " + serialized, 0); + if (node.getIndex() >= getNodeCount(nodeType)) { + throw new ParseException("Cannot index " + nodeType + " node " + node.getIndex() + " of " + getNodeCount(nodeType) + " in state: " + serialized, 0); } if (!nodeData.node.equals(node)) { nodeData.addNodeState(); @@ -289,7 +416,6 @@ public class ClusterState implements Cloneable { // Ignore unknown nodeStates } nodeData.addNodeState(); - removeLastNodesDownWithoutReason(); } public String getTextualDifference(ClusterState other) { @@ -363,7 +489,7 @@ public class ClusterState implements Cloneable { * E.g. if node X is down and without description, but nodex X-1 is up, then Y is 1. * The node count for distributors is then X + 1 - Y. */ - public int getNodeCount(NodeType type) { return nodeCount.get(type.ordinal()); } + public int getNodeCount(NodeType type) { return getNodes(type).getLogicalNodeCount(); } /** * Returns the state of a node. @@ -371,9 +497,7 @@ public class ClusterState implements Cloneable { * and DOWN otherwise. */ public NodeState getNodeState(Node node) { - if (node.getIndex() >= nodeCount.get(node.getType().ordinal())) - return new NodeState(node.getType(), State.DOWN); - return nodeStates.getOrDefault(node, new NodeState(node.getType(), State.UP)); + return getNodes(node.getType()).getNodeState(node.getIndex()); } /** @@ -383,35 +507,7 @@ public class ClusterState implements Cloneable { */ public void setNodeState(Node node, NodeState newState) { newState.verifyValidInSystemState(node.getType()); - if (node.getIndex() >= nodeCount.get(node.getType().ordinal())) { - for (int i= nodeCount.get(node.getType().ordinal()); i<node.getIndex(); ++i) { - nodeStates.put(new Node(node.getType(), i), new NodeState(node.getType(), State.DOWN)); - } - nodeCount.set(node.getType().ordinal(), node.getIndex() + 1); - } - if (newState.equals(new NodeState(node.getType(), State.UP))) { - nodeStates.remove(node); - } else { - nodeStates.put(node, newState); - } - if (newState.getState().equals(State.DOWN)) { - // We might be setting the last node down, so we can remove some states - removeLastNodesDownWithoutReason(); - } - } - - private void removeLastNodesDownWithoutReason() { - for (NodeType nodeType : NodeType.values()) { - for (int index = nodeCount.get(nodeType.ordinal()) - 1; index >= 0; --index) { - Node node = new Node(nodeType, index); - NodeState nodeState = nodeStates.get(node); - if (nodeState == null) break; // Node not existing is up - if ( ! nodeState.getState().equals(State.DOWN)) break; // Node not down can not be removed - if (nodeState.hasDescription()) break; // Node have reason to be down. Don't remove node as we will forget reason - nodeStates.remove(node); - nodeCount.set(nodeType.ordinal(), node.getIndex()); - } - } + getNodes(node.getType()).setNodeState(node, newState); } public String getDescription() { return description; } @@ -440,35 +536,9 @@ public class ClusterState implements Cloneable { sb.append(" bits:").append(distributionBits); } - int distributorNodeCount = getNodeCount(NodeType.DISTRIBUTOR); - int storageNodeCount = getNodeCount(NodeType.STORAGE); - // If not printing verbose, we're not printing descriptions, so we can remove tailing nodes that are down that has descriptions too - if (!verbose) { - while (distributorNodeCount > 0 && getNodeState(new Node(NodeType.DISTRIBUTOR, distributorNodeCount - 1)).getState().equals(State.DOWN)) --distributorNodeCount; - while (storageNodeCount > 0 && getNodeState(new Node(NodeType.STORAGE, storageNodeCount - 1)).getState().equals(State.DOWN)) --storageNodeCount; - } - if (distributorNodeCount > 0){ - sb.append(" distributor:").append(distributorNodeCount); - for (Map.Entry<Node, NodeState> entry : nodeStates.entrySet()) { - if (entry.getKey().getType().equals(NodeType.DISTRIBUTOR) && entry.getKey().getIndex() < distributorNodeCount) { - String nodeState = entry.getValue().serialize(entry.getKey().getIndex(), verbose); - if (!nodeState.isEmpty()) { - sb.append(' ').append(nodeState); - } - } - } - } - if (storageNodeCount > 0){ - sb.append(" storage:").append(storageNodeCount); - for (Map.Entry<Node, NodeState> entry : nodeStates.entrySet()) { - if (entry.getKey().getType().equals(NodeType.STORAGE) && entry.getKey().getIndex() < storageNodeCount) { - String nodeState = entry.getValue().serialize(entry.getKey().getIndex(), verbose); - if (!nodeState.isEmpty()) { - sb.append(' ').append(nodeState); - } - } - } - } + sb.append(distributorNodes.toString(verbose)); + sb.append(storageNodes.toString(verbose)); + if (sb.length() > 0) { // Remove first space if not empty sb.deleteCharAt(0); } diff --git a/vdslib/src/main/java/com/yahoo/vdslib/state/NodeState.java b/vdslib/src/main/java/com/yahoo/vdslib/state/NodeState.java index 0edf30bc1ff..80cf87b7597 100644 --- a/vdslib/src/main/java/com/yahoo/vdslib/state/NodeState.java +++ b/vdslib/src/main/java/com/yahoo/vdslib/state/NodeState.java @@ -51,6 +51,7 @@ public class NodeState implements Cloneable { public boolean equals(Object o) { if (!(o instanceof NodeState)) { return false; } NodeState ns = (NodeState) o; + // Note that 'description' is not considered as it carries semantics. if (state != ns.state || Math.abs(capacity - ns.capacity) > 0.0000000001 || Math.abs(initProgress - ns.initProgress) > 0.0000000001 diff --git a/vdslib/src/test/java/com/yahoo/vdslib/state/ClusterStateTestCase.java b/vdslib/src/test/java/com/yahoo/vdslib/state/ClusterStateTestCase.java index dbc8888cfda..956ea216a44 100644 --- a/vdslib/src/test/java/com/yahoo/vdslib/state/ClusterStateTestCase.java +++ b/vdslib/src/test/java/com/yahoo/vdslib/state/ClusterStateTestCase.java @@ -4,6 +4,7 @@ package com.yahoo.vdslib.state; import org.junit.Test; import java.text.ParseException; +import java.util.BitSet; import java.util.function.BiFunction; import static org.junit.Assert.assertEquals; @@ -311,6 +312,17 @@ public class ClusterStateTestCase{ } @Test + public void testBitSet() { + BitSet b = new BitSet(); + assertEquals(0, b.length()); + b.set(7); + b.set(107); + assertEquals(108, b.length()); + b.clear(107); + assertEquals(8, b.length()); + } + + @Test public void testVersionAndClusterStates() throws ParseException { ClusterState state = new ClusterState("version:4 cluster:i distributor:2 .1.s:i storage:2 .0.s:i .0.i:0.345"); assertEquals(4, state.getVersion()); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/common/ClientBase.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/common/ClientBase.java index acf580d7e1b..3acd1c34d51 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/common/ClientBase.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/common/ClientBase.java @@ -34,7 +34,7 @@ import java.util.logging.Logger; */ public abstract class ClientBase implements AutoCloseable { - private static final Logger logger = Logger.getLogger(ClientBase.class.getName()); + protected final Logger logger = Logger.getLogger(getClass().getName()); private static final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java index 297852e9584..769a2a54c95 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java @@ -1,7 +1,6 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.client.zms; -import com.yahoo.io.IOUtils; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzGroup; import com.yahoo.vespa.athenz.api.AthenzIdentity; @@ -18,7 +17,7 @@ import com.yahoo.vespa.athenz.client.zms.bindings.AssertionEntity; import com.yahoo.vespa.athenz.client.zms.bindings.DomainListResponseEntity; import com.yahoo.vespa.athenz.client.zms.bindings.MembershipEntity; import com.yahoo.vespa.athenz.client.zms.bindings.PolicyEntity; -import com.yahoo.vespa.athenz.client.zms.bindings.ProviderResourceGroupRolesRequestEntity; +import com.yahoo.vespa.athenz.client.zms.bindings.ResourceGroupRolesEntity; import com.yahoo.vespa.athenz.client.zms.bindings.ResponseListEntity; import com.yahoo.vespa.athenz.client.zms.bindings.RoleEntity; import com.yahoo.vespa.athenz.client.zms.bindings.ServiceEntity; @@ -33,11 +32,8 @@ import org.apache.http.entity.StringEntity; import org.apache.http.message.BasicHeader; import javax.net.ssl.SSLContext; -import java.io.IOException; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.time.Instant; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -104,7 +100,7 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient { HttpUriRequest request = RequestBuilder.put() .setUri(uri) .addHeader(createCookieHeaderWithOktaTokens(identityToken, accessToken)) - .setEntity(toJsonStringEntity(new ProviderResourceGroupRolesRequestEntity(providerService, tenantDomain, roleActions, resourceGroup))) + .setEntity(toJsonStringEntity(new ResourceGroupRolesEntity(providerService, tenantDomain, roleActions, resourceGroup))) .build(); execute(request, response -> readEntity(response, Void.class)); // Note: The ZMS API will actually return a json object that is similar to ProviderResourceGroupRolesRequestEntity } @@ -121,6 +117,31 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient { } @Override + public void createTenantResourceGroup(AthenzDomain tenantDomain, AthenzIdentity provider, String resourceGroup, + Set<RoleAction> roleActions) { + URI uri = zmsUrl.resolve(String.format("domain/%s/service/%s/tenant/%s/resourceGroup/%s", + provider.getDomainName(), provider.getName(), tenantDomain.getName(), resourceGroup)); + HttpUriRequest request = RequestBuilder.put() + .setUri(uri) + .setEntity(toJsonStringEntity( + new ResourceGroupRolesEntity(provider, tenantDomain, roleActions, resourceGroup))) + .build(); + execute(request, response -> readEntity(response, Void.class)); + } + + @Override + public Set<RoleAction> getTenantResourceGroups(AthenzDomain tenantDomain, AthenzIdentity provider, + String resourceGroup) { + URI uri = zmsUrl.resolve(String.format("domain/%s/service/%s/tenant/%s/resourceGroup/%s", + provider.getDomainName(), provider.getName(), tenantDomain.getName(), resourceGroup)); + HttpUriRequest request = RequestBuilder.get() + .setUri(uri) + .build(); + ResourceGroupRolesEntity result = execute(request, response -> readEntity(response, ResourceGroupRolesEntity.class)); + return result.roles.stream().map(rgr -> new RoleAction(rgr.role, rgr.action)).collect(Collectors.toSet()); + } + + @Override public void addRoleMember(AthenzRole role, AthenzIdentity member, Optional<String> reason) { URI uri = zmsUrl.resolve(String.format("domain/%s/role/%s/member/%s", role.domain().getName(), role.roleName(), member.getFullName())); MembershipEntity membership = new MembershipEntity.RoleMembershipEntity(member.getFullName(), true, role.roleName(), null); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java index 7dd0585bfd4..b1c26923113 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java @@ -34,6 +34,12 @@ public interface ZmsClient extends AutoCloseable { void deleteProviderResourceGroup(AthenzDomain tenantDomain, AthenzIdentity providerService, String resourceGroup, OktaIdentityToken identityToken, OktaAccessToken accessToken); + /** For manual tenancy provisioning - only creates roles/policies on provider domain */ + void createTenantResourceGroup(AthenzDomain tenantDomain, AthenzIdentity provider, String resourceGroup, + Set<RoleAction> roleActions); + + Set<RoleAction> getTenantResourceGroups(AthenzDomain tenantDomain, AthenzIdentity provider, String resourceGroup); + void addRoleMember(AthenzRole role, AthenzIdentity member, Optional<String> reason); void deleteRoleMember(AthenzRole role, AthenzIdentity member); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/ProviderResourceGroupRolesRequestEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/ResourceGroupRolesEntity.java index a67bd4dcad6..865dc8c02cb 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/ProviderResourceGroupRolesRequestEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/ResourceGroupRolesEntity.java @@ -1,39 +1,53 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.athenz.client.zms.bindings; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.client.zms.RoleAction; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import static java.util.stream.Collectors.toList; /** * @author bjorncs */ -public class ProviderResourceGroupRolesRequestEntity { +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResourceGroupRolesEntity { @JsonProperty("domain") - private final String domain; + public final String domain; @JsonProperty("service") - private final String service; + public final String service; @JsonProperty("tenant") - private final String tenant; + public final String tenant; @JsonProperty("roles") - private final List<TenantRoleAction> roles; + public final List<TenantRoleAction> roles; @JsonProperty("resourceGroup") - private final String resourceGroup; + public final String resourceGroup; - public ProviderResourceGroupRolesRequestEntity(AthenzIdentity providerService, AthenzDomain tenantDomain, Set<RoleAction> rolesActions, String resourceGroup) { + @JsonCreator + public ResourceGroupRolesEntity(@JsonProperty("domain") String domain, + @JsonProperty("service") String service, + @JsonProperty("tenant") String tenant, + @JsonProperty("roles") List<TenantRoleAction> roles, + @JsonProperty("resourceGroup") String resourceGroup) { + this.domain = domain; + this.service = service; + this.tenant = tenant; + this.roles = roles; + this.resourceGroup = resourceGroup; + } + + public ResourceGroupRolesEntity(AthenzIdentity providerService, AthenzDomain tenantDomain, Set<RoleAction> rolesActions, String resourceGroup) { this.domain = providerService.getDomainName(); this.service = providerService.getName(); this.tenant = tenantDomain.getName(); @@ -43,10 +57,10 @@ public class ProviderResourceGroupRolesRequestEntity { public static class TenantRoleAction { @JsonProperty("role") - private final String role; + public final String role; @JsonProperty("action") - private final String action; + public final String action; public TenantRoleAction(String role, String action) { this.role = role; diff --git a/vespajlib/abi-spec.json b/vespajlib/abi-spec.json index e5d5b8ba5b6..e68a37b15b6 100644 --- a/vespajlib/abi-spec.json +++ b/vespajlib/abi-spec.json @@ -1203,6 +1203,7 @@ "public com.yahoo.tensor.Tensor notEqual(com.yahoo.tensor.Tensor)", "public com.yahoo.tensor.Tensor approxEqual(com.yahoo.tensor.Tensor)", "public com.yahoo.tensor.Tensor bit(com.yahoo.tensor.Tensor)", + "public com.yahoo.tensor.Tensor hamming(com.yahoo.tensor.Tensor)", "public com.yahoo.tensor.Tensor avg()", "public com.yahoo.tensor.Tensor avg(java.lang.String)", "public com.yahoo.tensor.Tensor avg(java.util.List)", @@ -2189,6 +2190,22 @@ ], "fields": [] }, + "com.yahoo.tensor.functions.ScalarFunctions$Hamming": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.util.function.DoubleBinaryOperator" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void <init>()", + "public static double hamming(double, double)", + "public double applyAsDouble(double, double)", + "public java.lang.String toString()" + ], + "fields": [] + }, "com.yahoo.tensor.functions.ScalarFunctions$LeakyRelu": { "superClass": "java.lang.Object", "interfaces": [ @@ -2557,6 +2574,7 @@ "public static java.util.function.DoubleBinaryOperator pow()", "public static java.util.function.DoubleBinaryOperator squareddifference()", "public static java.util.function.DoubleBinaryOperator subtract()", + "public static java.util.function.DoubleBinaryOperator hamming()", "public static java.util.function.DoubleUnaryOperator abs()", "public static java.util.function.DoubleUnaryOperator acos()", "public static java.util.function.DoubleUnaryOperator asin()", diff --git a/vespajlib/src/main/java/com/yahoo/lang/MutableBoolean.java b/vespajlib/src/main/java/com/yahoo/lang/MutableBoolean.java index 17501b17bd0..877620547ba 100644 --- a/vespajlib/src/main/java/com/yahoo/lang/MutableBoolean.java +++ b/vespajlib/src/main/java/com/yahoo/lang/MutableBoolean.java @@ -22,6 +22,12 @@ public class MutableBoolean { public void orSet(boolean value) { this.value |= value; } + public boolean getAndSet(boolean newValue) { + boolean prev = value; + value = newValue; + return prev; + } + @Override public String toString() { return Boolean.toString(value); } diff --git a/vespajlib/src/main/java/com/yahoo/protect/Process.java b/vespajlib/src/main/java/com/yahoo/protect/Process.java index f3674f665b2..8038382c348 100644 --- a/vespajlib/src/main/java/com/yahoo/protect/Process.java +++ b/vespajlib/src/main/java/com/yahoo/protect/Process.java @@ -74,9 +74,13 @@ public final class Process { } } - public static void dumpHeap(String filePath, boolean live) throws IOException { + public static void dumpHeap(String filePath, boolean live) { log.log(Level.INFO, "Will dump the heap to '" + filePath + "', with the live = " + live); - getHotspotMXBean().dumpHeap(filePath, live); + try { + getHotspotMXBean().dumpHeap(filePath, live); + } catch (IOException e) { + log.log(Level.WARNING, "Failed writing heap dump:", e); + } } private static HotSpotDiagnosticMXBean getHotspotMXBean() throws IOException { diff --git a/vespajlib/src/main/java/com/yahoo/tensor/Tensor.java b/vespajlib/src/main/java/com/yahoo/tensor/Tensor.java index ab475e25387..3d4536d9249 100644 --- a/vespajlib/src/main/java/com/yahoo/tensor/Tensor.java +++ b/vespajlib/src/main/java/com/yahoo/tensor/Tensor.java @@ -36,6 +36,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import static com.yahoo.text.Ascii7BitMatcher.charsAndNumbers; +import static com.yahoo.tensor.functions.ScalarFunctions.Hamming; /** * A multidimensional array which can be used in computations. @@ -241,6 +242,7 @@ public interface Tensor { default Tensor notEqual(Tensor argument) { return join(argument, (a, b) -> ( a != b ? 1.0 : 0.0)); } default Tensor approxEqual(Tensor argument) { return join(argument, (a, b) -> ( approxEquals(a,b) ? 1.0 : 0.0)); } default Tensor bit(Tensor argument) { return join(argument, (a,b) -> ((int)b < 8 && (int)b >= 0 && ((int)a & (1 << (int)b)) != 0) ? 1.0 : 0.0); } + default Tensor hamming(Tensor argument) { return join(argument, (a,b) -> Hamming.hamming(a,b)); } default Tensor avg() { return avg(Collections.emptyList()); } default Tensor avg(String dimension) { return avg(Collections.singletonList(dimension)); } diff --git a/vespajlib/src/main/java/com/yahoo/tensor/functions/ScalarFunctions.java b/vespajlib/src/main/java/com/yahoo/tensor/functions/ScalarFunctions.java index 3ee9e67cdd6..d6fcd17b8fb 100644 --- a/vespajlib/src/main/java/com/yahoo/tensor/functions/ScalarFunctions.java +++ b/vespajlib/src/main/java/com/yahoo/tensor/functions/ScalarFunctions.java @@ -33,6 +33,7 @@ public class ScalarFunctions { public static DoubleBinaryOperator pow() { return new Pow(); } public static DoubleBinaryOperator squareddifference() { return new SquaredDifference(); } public static DoubleBinaryOperator subtract() { return new Subtract(); } + public static DoubleBinaryOperator hamming() { return new Hamming(); } public static DoubleUnaryOperator abs() { return new Abs(); } public static DoubleUnaryOperator acos() { return new Acos(); } @@ -152,6 +153,26 @@ public class ScalarFunctions { public String toString() { return "f(a,b)(a - b)"; } } + + public static class Hamming implements DoubleBinaryOperator { + public static double hamming(double left, double right) { + double distance = 0; + byte a = (byte) left; + byte b = (byte) right; + for (int i = 0; i < 8; i++) { + byte bit = (byte) (1 << i); + if ((a & bit) != (b & bit)) { + distance += 1; + } + } + return distance; + } + @Override + public double applyAsDouble(double left, double right) { return hamming(left, right); } + @Override + public String toString() { return "f(a,b)(hamming(a,b))"; } + } + // Unary operators ------------------------------------------------------------------------------ |