aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config-model/src/main/java/com/yahoo/schema/RankProfile.java112
-rw-r--r--config-model/src/main/java/com/yahoo/schema/derived/RawRankProfile.java21
-rw-r--r--config-model/src/main/java/com/yahoo/schema/expressiontransforms/ExpressionTransforms.java3
-rw-r--r--config-model/src/main/java/com/yahoo/schema/expressiontransforms/InputRecorder.java37
-rw-r--r--config-model/src/main/java/com/yahoo/schema/expressiontransforms/NormalizerFunctionExpander.java134
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/Handler.java4
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java9
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java11
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFiles.java5
-rw-r--r--config-model/src/test/derived/rankingexpression/rank-profiles.cfg62
-rw-r--r--config-model/src/test/derived/rankingexpression/rankexpression.sd28
-rw-r--r--config-model/src/test/java/com/yahoo/schema/NoNormalizersTestCase.java172
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java3
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/HandlerBuilderTest.java35
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFilesTest.java54
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java15
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java11
-rw-r--r--container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java4
-rw-r--r--container-search/src/main/java/com/yahoo/search/ranking/GlobalPhaseRanker.java17
-rw-r--r--container-search/src/main/java/com/yahoo/search/ranking/PreparedInput.java4
-rw-r--r--container-search/src/test/java/com/yahoo/search/ranking/GlobalPhaseRerankHitsImplTest.java238
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java44
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java33
-rw-r--r--controller-server/src/main/resources/mail/cloud-trial-notification.vm2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java30
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java15
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json11
-rw-r--r--controller-server/src/test/resources/mail/trial-expired.html646
-rw-r--r--controller-server/src/test/resources/mail/trial-expiring-immediately.html646
-rw-r--r--controller-server/src/test/resources/mail/trial-expiring-soon.html646
-rw-r--r--controller-server/src/test/resources/mail/trial-reminder.html646
-rw-r--r--controller-server/src/test/resources/mail/welcome.html646
-rw-r--r--documentapi/src/tests/policies/policies_test.cpp10
-rw-r--r--documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.cpp59
-rw-r--r--documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.h45
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java33
-rw-r--r--metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java7
-rw-r--r--metrics/src/main/java/ai/vespa/metrics/docs/MetricSetDocumentation.java8
-rw-r--r--metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java11
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java14
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java7
-rw-r--r--screwdriver.yaml2
-rw-r--r--storage/src/tests/storageserver/documentapiconvertertest.cpp2
-rw-r--r--storage/src/tests/storageserver/priorityconvertertest.cpp4
-rw-r--r--storage/src/tests/storageserver/testvisitormessagesession.h6
-rw-r--r--storage/src/tests/visiting/visitormanagertest.cpp2
-rw-r--r--storage/src/tests/visiting/visitortest.cpp2
-rw-r--r--storage/src/vespa/storage/storageserver/communicationmanager.cpp2
-rw-r--r--storage/src/vespa/storage/storageserver/documentapiconverter.cpp5
-rw-r--r--storage/src/vespa/storage/storageserver/documentapiconverter.h11
-rw-r--r--storage/src/vespa/storage/storageserver/priorityconverter.cpp132
-rw-r--r--storage/src/vespa/storage/storageserver/priorityconverter.h44
-rw-r--r--vespajlib/abi-spec.json13
-rw-r--r--vespajlib/src/main/java/ai/vespa/llm/LanguageModel.java4
-rw-r--r--vespajlib/src/main/java/ai/vespa/llm/client/openai/OpenAiClient.java90
-rw-r--r--vespajlib/src/main/java/ai/vespa/llm/completion/Completion.java4
-rw-r--r--vespajlib/src/main/java/ai/vespa/llm/test/MockLanguageModel.java7
-rw-r--r--vespajlib/src/test/java/ai/vespa/llm/client/openai/OpenAiClientCompletionTest.java21
-rw-r--r--vespalib/src/vespa/fastos/linux_file.cpp22
-rw-r--r--vespalib/src/vespa/fastos/linux_file.h8
75 files changed, 4723 insertions, 402 deletions
diff --git a/config-model/src/main/java/com/yahoo/schema/RankProfile.java b/config-model/src/main/java/com/yahoo/schema/RankProfile.java
index 6007a1cf4b1..e2577f4f834 100644
--- a/config-model/src/main/java/com/yahoo/schema/RankProfile.java
+++ b/config-model/src/main/java/com/yahoo/schema/RankProfile.java
@@ -22,6 +22,7 @@ import com.yahoo.searchlib.rankingexpression.FeatureList;
import com.yahoo.searchlib.rankingexpression.RankingExpression;
import com.yahoo.searchlib.rankingexpression.Reference;
import com.yahoo.searchlib.rankingexpression.rule.Arguments;
+import com.yahoo.searchlib.rankingexpression.rule.ExpressionNode;
import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode;
import com.yahoo.tensor.Tensor;
import com.yahoo.tensor.TensorType;
@@ -30,6 +31,7 @@ import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -1058,21 +1060,45 @@ public class RankProfile implements Cloneable {
functions = compileFunctions(this::getFunctions, queryProfiles, featureTypes, importedModels, inlineFunctions, expressionTransforms);
allFunctionsCached = null;
+ var context = new RankProfileTransformContext(this,
+ queryProfiles,
+ featureTypes,
+ importedModels,
+ constants(),
+ inlineFunctions);
+ var allNormalizers = getFeatureNormalizers();
+ verifyNoNormalizers("first-phase expression", firstPhaseRanking, allNormalizers, context);
+ verifyNoNormalizers("second-phase expression", secondPhaseRanking, allNormalizers, context);
+ for (ReferenceNode mf : getMatchFeatures()) {
+ verifyNoNormalizers("match-feature " + mf, mf, allNormalizers, context);
+ }
+ for (ReferenceNode sf : getSummaryFeatures()) {
+ verifyNoNormalizers("summary-feature " + sf, sf, allNormalizers, context);
+ }
if (globalPhaseRanking != null) {
- var context = new RankProfileTransformContext(this,
- queryProfiles,
- featureTypes,
- importedModels,
- constants(),
- inlineFunctions);
var needInputs = new HashSet<String>();
+ Set<String> userDeclaredMatchFeatures = new HashSet<>();
+ for (ReferenceNode mf : getMatchFeatures()) {
+ userDeclaredMatchFeatures.add(mf.toString());
+ }
var recorder = new InputRecorder(needInputs);
- if (matchFeatures != null) {
- for (ReferenceNode mf : matchFeatures) {
- recorder.alreadyHandled(mf.toString());
+ recorder.alreadyMatchFeatures(userDeclaredMatchFeatures);
+ recorder.addKnownNormalizers(allNormalizers.keySet());
+ recorder.process(globalPhaseRanking.function().getBody(), context);
+ for (var normalizerName : recorder.normalizersUsed()) {
+ var normalizer = allNormalizers.get(normalizerName);
+ var func = functions.get(normalizer.input());
+ if (func != null) {
+ verifyNoNormalizers("normalizer input " + normalizer.input(), func, allNormalizers, context);
+ if (! userDeclaredMatchFeatures.contains(normalizer.input())) {
+ var subRecorder = new InputRecorder(needInputs);
+ subRecorder.alreadyMatchFeatures(userDeclaredMatchFeatures);
+ subRecorder.process(func.function().getBody(), context);
+ }
+ } else {
+ needInputs.add(normalizer.input());
}
}
- recorder.process(globalPhaseRanking.function().getBody(), context);
List<FeatureList> addIfMissing = new ArrayList<>();
for (String input : needInputs) {
if (input.startsWith("constant(") || input.startsWith("query(")) {
@@ -1630,4 +1656,70 @@ public class RankProfile implements Cloneable {
}
+ public static record RankFeatureNormalizer(Reference original, String name, String input, String algo, double kparam) {
+ @Override
+ public String toString() {
+ return "normalizer{name=" + name + ",input=" + input + ",algo=" + algo + ",k=" + kparam + "}";
+ }
+ private static long hash(String s) {
+ int bob = com.yahoo.collections.BobHash.hash(s);
+ return bob + 0x100000000L;
+ }
+ public static RankFeatureNormalizer linear(Reference original, Reference inputRef) {
+ long h = hash(original.toString());
+ String name = "normalize@" + h + "@linear";
+ return new RankFeatureNormalizer(original, name, inputRef.toString(), "LINEAR", 0.0);
+ }
+ public static RankFeatureNormalizer rrank(Reference original, Reference inputRef, double k) {
+ long h = hash(original.toString());
+ String name = "normalize@" + h + "@rrank";
+ return new RankFeatureNormalizer(original, name, inputRef.toString(), "RRANK", k);
+ }
+ }
+
+ private List<RankFeatureNormalizer> featureNormalizers = new ArrayList<>();
+
+ public Map<String, RankFeatureNormalizer> getFeatureNormalizers() {
+ Map<String, RankFeatureNormalizer> all = new LinkedHashMap<>();
+ for (var inheritedProfile : inherited()) {
+ all.putAll(inheritedProfile.getFeatureNormalizers());
+ }
+ for (var n : featureNormalizers) {
+ all.put(n.name(), n);
+ }
+ return all;
+ }
+
+ public void addFeatureNormalizer(RankFeatureNormalizer n) {
+ if (functions.get(n.name()) != null) {
+ throw new IllegalArgumentException("cannot use name '" + name + "' for both function and normalizer");
+ }
+ featureNormalizers.add(n);
+ }
+
+ private void verifyNoNormalizers(String where, RankingExpressionFunction f, Map<String, RankFeatureNormalizer> allNormalizers, RankProfileTransformContext context) {
+ if (f == null) return;
+ verifyNoNormalizers(where, f.function(), allNormalizers, context);
+ }
+
+ private void verifyNoNormalizers(String where, ExpressionFunction func, Map<String, RankFeatureNormalizer> allNormalizers, RankProfileTransformContext context) {
+ if (func == null) return;
+ var body = func.getBody();
+ if (body == null) return;
+ verifyNoNormalizers(where, body.getRoot(), allNormalizers, context);
+ }
+
+ private void verifyNoNormalizers(String where, ExpressionNode node, Map<String, RankFeatureNormalizer> allNormalizers, RankProfileTransformContext context) {
+ var needInputs = new HashSet<String>();
+ var recorder = new InputRecorder(needInputs);
+ recorder.process(node, context);
+ for (var input : needInputs) {
+ var normalizer = allNormalizers.get(input);
+ if (normalizer != null) {
+ throw new IllegalArgumentException("Cannot use " + normalizer.original() + " from " + where + ", only valid in global-phase expression");
+ }
+ }
+ }
+
+
}
diff --git a/config-model/src/main/java/com/yahoo/schema/derived/RawRankProfile.java b/config-model/src/main/java/com/yahoo/schema/derived/RawRankProfile.java
index 05e5f17ea3d..eb9f7d44c91 100644
--- a/config-model/src/main/java/com/yahoo/schema/derived/RawRankProfile.java
+++ b/config-model/src/main/java/com/yahoo/schema/derived/RawRankProfile.java
@@ -54,6 +54,7 @@ public class RawRankProfile implements RankProfilesConfig.Producer {
private final String name;
private final Compressor.Compression compressedProperties;
+ private final Map<String, RankProfile.RankFeatureNormalizer> featureNormalizers;
/** The compiled profile this is created from. */
private final Collection<RankProfile.Constant> constants;
@@ -66,13 +67,14 @@ public class RawRankProfile implements RankProfilesConfig.Producer {
this.name = rankProfile.name();
/*
* Forget the RankProfiles as soon as possible. They can become very large and memory hungry
- * Especially do not refer then through any member variables due to the RawRankProfile living forever.
+ * Especially do not refer them through any member variables due to the RawRankProfile living forever.
*/
RankProfile compiled = rankProfile.compile(queryProfiles, importedModels);
constants = compiled.constants().values();
onnxModels = compiled.onnxModels().values();
- compressedProperties = compress(new Deriver(compiled, attributeFields, deployProperties, queryProfiles)
- .derive(largeExpressions));
+ var deriver = new Deriver(compiled, attributeFields, deployProperties, queryProfiles);
+ compressedProperties = compress(deriver.derive(largeExpressions));
+ this.featureNormalizers = compiled.getFeatureNormalizers();
}
public Collection<RankProfile.Constant> constants() { return constants; }
@@ -111,6 +113,18 @@ public class RawRankProfile implements RankProfilesConfig.Producer {
b.fef(fefB);
}
+ private void buildNormalizers(RankProfilesConfig.Rankprofile.Builder b) {
+ for (var normalizer : featureNormalizers.values()) {
+ var nBuilder = new RankProfilesConfig.Rankprofile.Normalizer.Builder();
+ nBuilder.name(normalizer.name());
+ nBuilder.input(normalizer.input());
+ var algo = RankProfilesConfig.Rankprofile.Normalizer.Algo.Enum.valueOf(normalizer.algo());
+ nBuilder.algo(algo);
+ nBuilder.kparam(normalizer.kparam());
+ b.normalizer(nBuilder);
+ }
+ }
+
/**
* Returns the properties of this as an unmodifiable list.
* Note: This method is expensive.
@@ -121,6 +135,7 @@ public class RawRankProfile implements RankProfilesConfig.Producer {
public void getConfig(RankProfilesConfig.Builder builder) {
RankProfilesConfig.Rankprofile.Builder b = new RankProfilesConfig.Rankprofile.Builder().name(getName());
getRankProperties(b);
+ buildNormalizers(b);
builder.rankprofile(b);
}
diff --git a/config-model/src/main/java/com/yahoo/schema/expressiontransforms/ExpressionTransforms.java b/config-model/src/main/java/com/yahoo/schema/expressiontransforms/ExpressionTransforms.java
index cf46bedf223..42c8147b3dc 100644
--- a/config-model/src/main/java/com/yahoo/schema/expressiontransforms/ExpressionTransforms.java
+++ b/config-model/src/main/java/com/yahoo/schema/expressiontransforms/ExpressionTransforms.java
@@ -35,7 +35,8 @@ public class ExpressionTransforms {
new FunctionShadower(),
new TensorMaxMinTransformer(),
new Simplifier(),
- new BooleanExpressionTransformer());
+ new BooleanExpressionTransformer(),
+ new NormalizerFunctionExpander());
}
public RankingExpression transform(RankingExpression expression, RankProfileTransformContext context) {
diff --git a/config-model/src/main/java/com/yahoo/schema/expressiontransforms/InputRecorder.java b/config-model/src/main/java/com/yahoo/schema/expressiontransforms/InputRecorder.java
index 1128aaf3681..ab18f9c83db 100644
--- a/config-model/src/main/java/com/yahoo/schema/expressiontransforms/InputRecorder.java
+++ b/config-model/src/main/java/com/yahoo/schema/expressiontransforms/InputRecorder.java
@@ -14,6 +14,7 @@ import com.yahoo.searchlib.rankingexpression.transform.ExpressionTransformer;
import com.yahoo.tensor.functions.Generate;
import java.io.StringReader;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
@@ -29,19 +30,35 @@ public class InputRecorder extends ExpressionTransformer<InputRecorderContext> {
private final Set<String> neededInputs;
private final Set<String> handled = new HashSet<>();
+ private final Set<String> availableNormalizers = new HashSet<>();
+ private final Set<String> usedNormalizers = new HashSet<>();
public InputRecorder(Set<String> target) {
this.neededInputs = target;
}
public void process(RankingExpression expression, RankProfileTransformContext context) {
- transform(expression.getRoot(), new InputRecorderContext(context));
+ process(expression.getRoot(), context);
}
- public void alreadyHandled(String name) {
- handled.add(name);
+ public void process(ExpressionNode node, RankProfileTransformContext context) {
+ transform(node, new InputRecorderContext(context));
}
+ public void alreadyMatchFeatures(Collection<String> matchFeatures) {
+ for (String mf : matchFeatures) {
+ handled.add(mf);
+ }
+ }
+
+ public void addKnownNormalizers(Collection<String> names) {
+ for (String name : names) {
+ availableNormalizers.add(name);
+ }
+ }
+
+ public Set<String> normalizersUsed() { return this.usedNormalizers; }
+
@Override
public ExpressionNode transform(ExpressionNode node, InputRecorderContext context) {
if (node instanceof ReferenceNode r) {
@@ -77,6 +94,10 @@ public class InputRecorder extends ExpressionTransformer<InputRecorderContext> {
if (simpleFunctionOrIdentifier && context.localVariables().contains(name)) {
return;
}
+ if (simpleFunctionOrIdentifier && availableNormalizers.contains(name)) {
+ usedNormalizers.add(name);
+ return;
+ }
if (ref.isSimpleRankingExpressionWrapper()) {
name = ref.simpleArgument().get();
simpleFunctionOrIdentifier = true;
@@ -113,13 +134,21 @@ public class InputRecorder extends ExpressionTransformer<InputRecorderContext> {
}
}
if ("onnx".equals(name)) {
- if (args.size() != 1) {
+ if (args.size() < 1) {
throw new IllegalArgumentException("expected name of ONNX model as argument: " + feature);
}
var arg = args.expressions().get(0);
var models = context.rankProfile().onnxModels();
var model = models.get(arg.toString());
if (model == null) {
+ var tmp = OnnxModelTransformer.transformFeature(feature, context.rankProfile());
+ if (tmp instanceof ReferenceNode newRefNode) {
+ args = newRefNode.getArguments();
+ arg = args.expressions().get(0);
+ model = models.get(arg.toString());
+ }
+ }
+ if (model == null) {
throw new IllegalArgumentException("missing onnx model: " + arg);
}
model.getInputMap().forEach((__, onnxInput) -> {
diff --git a/config-model/src/main/java/com/yahoo/schema/expressiontransforms/NormalizerFunctionExpander.java b/config-model/src/main/java/com/yahoo/schema/expressiontransforms/NormalizerFunctionExpander.java
new file mode 100644
index 00000000000..a8fee966656
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/schema/expressiontransforms/NormalizerFunctionExpander.java
@@ -0,0 +1,134 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema.expressiontransforms;
+
+import com.yahoo.schema.FeatureNames;
+import com.yahoo.schema.RankProfile.RankFeatureNormalizer;
+import com.yahoo.searchlib.rankingexpression.evaluation.BooleanValue;
+import com.yahoo.searchlib.rankingexpression.rule.OperationNode;
+import com.yahoo.searchlib.rankingexpression.rule.Operator;
+import com.yahoo.searchlib.rankingexpression.rule.CompositeNode;
+import com.yahoo.searchlib.rankingexpression.rule.ConstantNode;
+import com.yahoo.searchlib.rankingexpression.rule.ExpressionNode;
+import com.yahoo.searchlib.rankingexpression.rule.IfNode;
+import com.yahoo.searchlib.rankingexpression.transform.ExpressionTransformer;
+import com.yahoo.searchlib.rankingexpression.transform.TransformContext;
+import com.yahoo.searchlib.rankingexpression.RankingExpression;
+import com.yahoo.searchlib.rankingexpression.Reference;
+import com.yahoo.searchlib.rankingexpression.parser.ParseException;
+import com.yahoo.searchlib.rankingexpression.rule.CompositeNode;
+import com.yahoo.searchlib.rankingexpression.rule.ConstantNode;
+import com.yahoo.searchlib.rankingexpression.rule.ExpressionNode;
+import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode;
+import com.yahoo.searchlib.rankingexpression.rule.TensorFunctionNode;
+import com.yahoo.searchlib.rankingexpression.transform.ExpressionTransformer;
+import com.yahoo.tensor.functions.Generate;
+
+import java.io.StringReader;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Recognizes pseudo-functions and creates global-phase normalizers
+ * @author arnej
+ */
+public class NormalizerFunctionExpander extends ExpressionTransformer<RankProfileTransformContext> {
+
+ public final static String NORMALIZE_LINEAR = "normalize_linear";
+ public final static String RECIPROCAL_RANK = "reciprocal_rank";
+ public final static String RECIPROCAL_RANK_FUSION = "reciprocal_rank_fusion";
+
+ @Override
+ public ExpressionNode transform(ExpressionNode node, RankProfileTransformContext context) {
+ if (node instanceof ReferenceNode r) {
+ node = transformReference(r, context);
+ }
+ if (node instanceof CompositeNode composite) {
+ node = transformChildren(composite, context);
+ }
+ return node;
+ }
+
+ private ExpressionNode transformReference(ReferenceNode node, RankProfileTransformContext context) {
+ Reference ref = node.reference();
+ String name = ref.name();
+ if (ref.output() != null) {
+ return node;
+ }
+ var f = context.rankProfile().getFunctions().get(name);
+ if (f != null) {
+ // never transform declared functions
+ return node;
+ }
+ return switch(name) {
+ case RECIPROCAL_RANK_FUSION -> transform(expandRRF(ref), context);
+ case NORMALIZE_LINEAR -> transformNormLin(ref, context);
+ case RECIPROCAL_RANK -> transformRRank(ref, context);
+ default -> node;
+ };
+ }
+
+ private ExpressionNode expandRRF(Reference ref) {
+ var args = ref.arguments();
+ if (args.size() < 2) {
+ throw new IllegalArgumentException("must have at least 2 arguments: " + ref);
+ }
+ List<ExpressionNode> children = new ArrayList<>();
+ List<Operator> operators = new ArrayList<>();
+ for (var arg : args.expressions()) {
+ if (! children.isEmpty()) operators.add(Operator.plus);
+ children.add(new ReferenceNode(RECIPROCAL_RANK, List.of(arg), null));
+ }
+ // must be further transformed (see above)
+ return new OperationNode(children, operators);
+ }
+
+ private ExpressionNode transformNormLin(Reference ref, RankProfileTransformContext context) {
+ var args = ref.arguments();
+ if (args.size() != 1) {
+ throw new IllegalArgumentException("must have exactly 1 argument: " + ref);
+ }
+ var input = args.expressions().get(0);
+ if (input instanceof ReferenceNode inputRefNode) {
+ var inputRef = inputRefNode.reference();
+ RankFeatureNormalizer normalizer = RankFeatureNormalizer.linear(ref, inputRef);
+ context.rankProfile().addFeatureNormalizer(normalizer);
+ var newRef = Reference.fromIdentifier(normalizer.name());
+ return new ReferenceNode(newRef);
+ } else {
+ throw new IllegalArgumentException("the first argument must be a simple feature: " + ref + " => " + input.getClass());
+ }
+ }
+
+ private ExpressionNode transformRRank(Reference ref, RankProfileTransformContext context) {
+ var args = ref.arguments();
+ if (args.size() < 1 || args.size() > 2) {
+ throw new IllegalArgumentException("must have 1 or 2 arguments: " + ref);
+ }
+ double k = 60.0;
+ if (args.size() == 2) {
+ var kArg = args.expressions().get(1);
+ if (kArg instanceof ConstantNode kNode) {
+ k = kNode.getValue().asDouble();
+ } else {
+ throw new IllegalArgumentException("the second argument (k) must be a constant in: " + ref);
+ }
+ }
+ var input = args.expressions().get(0);
+ if (input instanceof ReferenceNode inputRefNode) {
+ var inputRef = inputRefNode.reference();
+ RankFeatureNormalizer normalizer = RankFeatureNormalizer.rrank(ref, inputRef, k);
+ context.rankProfile().addFeatureNormalizer(normalizer);
+ var newRef = Reference.fromIdentifier(normalizer.name());
+ return new ReferenceNode(newRef);
+ } else {
+ throw new IllegalArgumentException("the first argument must be a simple feature: " + ref);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/Handler.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/Handler.java
index 0af970e016a..099255975b6 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/component/Handler.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/Handler.java
@@ -64,8 +64,8 @@ public class Handler extends Component<Component<?, ?>, ComponentModel> {
clientBindings.addAll(Arrays.asList(bindings));
}
- public final Set<BindingPattern> getServerBindings() {
- return Collections.unmodifiableSet(serverBindings);
+ public final Collection<BindingPattern> getServerBindings() {
+ return List.copyOf(serverBindings);
}
public final List<BindingPattern> getClientBindings() {
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java
index d2faff7850b..b14495756c3 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java
@@ -9,6 +9,7 @@ import com.yahoo.jdisc.http.ServerConfig;
import com.yahoo.osgi.provider.model.ComponentModel;
import com.yahoo.vespa.model.container.ApplicationContainerCluster;
import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.ConnectionLogComponent;
import com.yahoo.vespa.model.container.component.SimpleComponent;
import java.util.ArrayList;
@@ -24,13 +25,11 @@ import java.util.TreeSet;
public class JettyHttpServer extends SimpleComponent implements ServerConfig.Producer {
private final ContainerCluster<?> cluster;
- private volatile boolean isHostedVespa;
private final List<ConnectorFactory> connectorFactories = new ArrayList<>();
private final SortedSet<String> ignoredUserAgentsList = new TreeSet<>();
public JettyHttpServer(String componentId, ContainerCluster<?> cluster, DeployState deployState) {
super(new ComponentModel(componentId, com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName(), null));
- this.isHostedVespa = deployState.isHosted();
this.cluster = cluster;
FilterBindingsProviderComponent filterBindingsProviderComponent = new FilterBindingsProviderComponent(componentId);
addChild(filterBindingsProviderComponent);
@@ -42,8 +41,6 @@ public class JettyHttpServer extends SimpleComponent implements ServerConfig.Pro
}
}
- public void setHostedVespa(boolean isHostedVespa) { this.isHostedVespa = isHostedVespa; }
-
public void addConnector(ConnectorFactory connectorFactory) {
connectorFactories.add(connectorFactory);
addChild(connectorFactory);
@@ -64,10 +61,8 @@ public class JettyHttpServer extends SimpleComponent implements ServerConfig.Pro
.ignoredUserAgents(ignoredUserAgentsList)
.searchHandlerPaths(List.of("/search"))
);
- if (isHostedVespa) {
- // Enable connection log hosted Vespa
+ if (cluster.getAllComponents().stream().anyMatch(c -> c instanceof ConnectionLogComponent))
builder.connectionLog(new ServerConfig.ConnectionLog.Builder().enabled(true));
- }
configureJettyThreadpool(builder);
builder.stopTimeout(300);
}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java
index 7653d814d8a..119a3ad18c2 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java
@@ -3,19 +3,14 @@ package com.yahoo.vespa.model.container.xml;
import com.yahoo.config.model.ConfigModelContext;
import com.yahoo.config.model.deploy.DeployState;
-import com.yahoo.container.logging.AccessLog;
import com.yahoo.container.logging.FileConnectionLog;
-import com.yahoo.jdisc.http.server.jetty.VoidRequestLog;
import com.yahoo.vespa.model.container.ApplicationContainerCluster;
import com.yahoo.vespa.model.container.ContainerModel;
-import com.yahoo.vespa.model.container.component.AccessLogComponent;
import com.yahoo.vespa.model.container.component.ConnectionLogComponent;
import com.yahoo.vespa.model.container.configserver.ConfigserverCluster;
import com.yahoo.vespa.model.container.configserver.option.CloudConfigOptions;
import org.w3c.dom.Element;
-import static com.yahoo.vespa.model.container.component.AccessLogComponent.AccessLogType.jsonAccessLog;
-
/**
* Builds the config model for the standalone config server.
*
@@ -57,12 +52,6 @@ public class ConfigServerContainerModelBuilder extends ContainerModelBuilder {
}
@Override
- protected void addHttp(DeployState deployState, Element spec, ApplicationContainerCluster cluster, ConfigModelContext context) {
- super.addHttp(deployState, spec, cluster, context);
- cluster.getHttp().getHttpServer().get().setHostedVespa(isHosted());
- }
-
- @Override
protected void addModelEvaluationRuntime(ApplicationContainerCluster cluster) {
// Model evaluation bundles are pre-installed in the standalone container.
}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFiles.java b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFiles.java
index b10da29ee04..a454c1141ca 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFiles.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFiles.java
@@ -26,6 +26,7 @@ import java.util.Optional;
import java.util.logging.Level;
import static com.yahoo.vespa.model.container.ApplicationContainerCluster.UserConfiguredUrls;
+import static java.util.logging.Level.WARNING;
/**
* Utility methods for registering file distribution of files/paths/urls/models defined by the user.
@@ -74,7 +75,7 @@ public class UserConfiguredFiles implements Serializable {
if (configDefinition == null) {
String message = "Unable to find config definition " + key + ". Will not register files for file distribution for this config";
switch (unknownConfigDefinition) {
- case "warning" -> logger.logApplicationPackage(Level.WARNING, message);
+ case "warning" -> logger.logApplicationPackage(WARNING, message);
case "fail" -> throw new IllegalArgumentException("Unable to find config definition for " + key);
}
return;
@@ -162,7 +163,7 @@ public class UserConfiguredFiles implements Serializable {
ApplicationFile file = applicationPackage.getFile(path);
if (file.isDirectory() && (file.listFiles() == null || file.listFiles().isEmpty()))
- throw new IllegalArgumentException("Directory '" + path.getRelative() + "' is empty");
+ logger.logApplicationPackage(WARNING, "Directory '" + path.getRelative() + "' is empty");
FileReference reference = registeredFiles.get(path);
if (reference == null) {
diff --git a/config-model/src/test/derived/rankingexpression/rank-profiles.cfg b/config-model/src/test/derived/rankingexpression/rank-profiles.cfg
index b0f7d0f2477..b3257c962dd 100644
--- a/config-model/src/test/derived/rankingexpression/rank-profiles.cfg
+++ b/config-model/src/test/derived/rankingexpression/rank-profiles.cfg
@@ -520,3 +520,65 @@ rankprofile[].fef.property[].name "vespa.type.attribute.t1"
rankprofile[].fef.property[].value "tensor(m{},v[3])"
rankprofile[].fef.property[].name "vespa.type.query.v"
rankprofile[].fef.property[].value "tensor(v[3])"
+rankprofile[].name "withnorm"
+rankprofile[].fef.property[].name "rankingExpression(normBar).rankingScript"
+rankprofile[].fef.property[].value "attribute(foo1) + attribute(year)"
+rankprofile[].fef.property[].name "vespa.rank.firstphase"
+rankprofile[].fef.property[].value "attribute(foo1)"
+rankprofile[].fef.property[].name "vespa.rank.globalphase"
+rankprofile[].fef.property[].value "rankingExpression(globalphase)"
+rankprofile[].fef.property[].name "rankingExpression(globalphase).rankingScript"
+rankprofile[].fef.property[].value "normalize@3551296680@linear + normalize@2879443254@rrank"
+rankprofile[].fef.property[].name "vespa.match.feature"
+rankprofile[].fef.property[].value "nativeRank"
+rankprofile[].fef.property[].name "vespa.match.feature"
+rankprofile[].fef.property[].value "attribute(year)"
+rankprofile[].fef.property[].name "vespa.match.feature"
+rankprofile[].fef.property[].value "attribute(foo1)"
+rankprofile[].fef.property[].name "vespa.hidden.matchfeature"
+rankprofile[].fef.property[].value "attribute(year)"
+rankprofile[].fef.property[].name "vespa.hidden.matchfeature"
+rankprofile[].fef.property[].value "attribute(foo1)"
+rankprofile[].fef.property[].name "vespa.globalphase.rerankcount"
+rankprofile[].fef.property[].value "123"
+rankprofile[].fef.property[].name "vespa.type.attribute.t1"
+rankprofile[].fef.property[].value "tensor(m{},v[3])"
+rankprofile[].normalizer[].name "normalize@3551296680@linear"
+rankprofile[].normalizer[].input "nativeRank"
+rankprofile[].normalizer[].algo LINEAR
+rankprofile[].normalizer[].kparam 0.0
+rankprofile[].normalizer[].name "normalize@2879443254@rrank"
+rankprofile[].normalizer[].input "normBar"
+rankprofile[].normalizer[].algo RRANK
+rankprofile[].normalizer[].kparam 42.0
+rankprofile[].name "withfusion"
+rankprofile[].fef.property[].name "rankingExpression(normBar).rankingScript"
+rankprofile[].fef.property[].value "attribute(foo1) + attribute(year)"
+rankprofile[].fef.property[].name "vespa.rank.firstphase"
+rankprofile[].fef.property[].value "attribute(foo1)"
+rankprofile[].fef.property[].name "vespa.rank.globalphase"
+rankprofile[].fef.property[].value "rankingExpression(globalphase)"
+rankprofile[].fef.property[].name "rankingExpression(globalphase).rankingScript"
+rankprofile[].fef.property[].value "normalize@5385018767@rrank + normalize@3221316369@rrank"
+rankprofile[].fef.property[].name "vespa.match.feature"
+rankprofile[].fef.property[].value "nativeRank"
+rankprofile[].fef.property[].name "vespa.match.feature"
+rankprofile[].fef.property[].value "attribute(year)"
+rankprofile[].fef.property[].name "vespa.match.feature"
+rankprofile[].fef.property[].value "attribute(foo1)"
+rankprofile[].fef.property[].name "vespa.hidden.matchfeature"
+rankprofile[].fef.property[].value "attribute(year)"
+rankprofile[].fef.property[].name "vespa.hidden.matchfeature"
+rankprofile[].fef.property[].value "attribute(foo1)"
+rankprofile[].fef.property[].name "vespa.globalphase.rerankcount"
+rankprofile[].fef.property[].value "456"
+rankprofile[].fef.property[].name "vespa.type.attribute.t1"
+rankprofile[].fef.property[].value "tensor(m{},v[3])"
+rankprofile[].normalizer[].name "normalize@5385018767@rrank"
+rankprofile[].normalizer[].input "normBar"
+rankprofile[].normalizer[].algo RRANK
+rankprofile[].normalizer[].kparam 60.0
+rankprofile[].normalizer[].name "normalize@3221316369@rrank"
+rankprofile[].normalizer[].input "nativeRank"
+rankprofile[].normalizer[].algo RRANK
+rankprofile[].normalizer[].kparam 60.0
diff --git a/config-model/src/test/derived/rankingexpression/rankexpression.sd b/config-model/src/test/derived/rankingexpression/rankexpression.sd
index 16dff61b63a..15537f1f9d0 100644
--- a/config-model/src/test/derived/rankingexpression/rankexpression.sd
+++ b/config-model/src/test/derived/rankingexpression/rankexpression.sd
@@ -441,4 +441,32 @@ schema rankexpression {
}
}
+ rank-profile withnorm {
+ first-phase {
+ expression: attribute(foo1)
+ }
+ function normBar() {
+ expression: attribute(foo1) + attribute(year)
+ }
+ global-phase {
+ expression: normalize_linear(nativeRank) + reciprocal_rank(normBar(), 42.0)
+ rerank-count: 123
+ }
+ match-features: nativeRank
+ }
+
+ rank-profile withfusion {
+ first-phase {
+ expression: attribute(foo1)
+ }
+ function normBar() {
+ expression: attribute(foo1) + attribute(year)
+ }
+ global-phase {
+ expression: reciprocal_rank_fusion(normBar, nativeRank)
+ rerank-count: 456
+ }
+ match-features: nativeRank
+ }
+
}
diff --git a/config-model/src/test/java/com/yahoo/schema/NoNormalizersTestCase.java b/config-model/src/test/java/com/yahoo/schema/NoNormalizersTestCase.java
new file mode 100644
index 00000000000..6e2efadfa2c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/schema/NoNormalizersTestCase.java
@@ -0,0 +1,172 @@
+// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.schema;
+
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.schema.parser.ParseException;
+import com.yahoo.yolean.Exceptions;
+import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests rank profiles with normalizers in bad places
+ *
+ * @author arnej
+ */
+public class NoNormalizersTestCase extends AbstractSchemaTestCase {
+
+ void compileSchema(String schema) throws ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ var qp = new QueryProfileRegistry();
+ ApplicationBuilder builder = new ApplicationBuilder(registry, qp);
+ builder.addSchema(schema);
+ builder.build(true);
+ for (RankProfile rp : registry.all()) {
+ rp.compile(qp, new ImportedMlModels());
+ }
+ }
+
+ @Test
+ void requireThatNormalizerInFirstPhaseIsChecked() throws ParseException {
+ try {
+ compileSchema("""
+ search test {
+ document test { }
+ rank-profile p1 {
+ first-phase {
+ expression: normalize_linear(nativeRank)
+ }
+ }
+ }
+ """);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Rank profile 'p1' is invalid: " +
+ "Cannot use normalize_linear(nativeRank) from first-phase expression, only valid in global-phase expression",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ void requireThatNormalizerInSecondPhaseIsChecked() throws ParseException {
+ try {
+ compileSchema("""
+ search test {
+ document test {
+ field title type string {
+ indexing: index
+ }
+ }
+ rank-profile p2 {
+ function foobar() {
+ expression: 42 + reciprocal_rank(whatever, 1.0)
+ }
+ function whatever() {
+ expression: fieldMatch(title)
+ }
+ first-phase {
+ expression: nativeRank
+ }
+ second-phase {
+ expression: foobar
+ }
+ }
+ }
+ """);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Rank profile 'p2' is invalid: " +
+ "Cannot use reciprocal_rank(whatever,1.0) from second-phase expression, only valid in global-phase expression",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ void requireThatNormalizerInMatchFeatureIsChecked() throws ParseException {
+ try {
+ compileSchema("""
+ search test {
+ document test { }
+ rank-profile p3 {
+ function foobar() {
+ expression: normalize_linear(nativeRank)
+ }
+ first-phase {
+ expression: nativeRank
+ }
+ match-features {
+ nativeRank
+ foobar
+ }
+ }
+ }
+ """);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Rank profile 'p3' is invalid: " +
+ "Cannot use normalize_linear(nativeRank) from match-feature foobar, only valid in global-phase expression",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ void requireThatNormalizerInSummaryFeatureIsChecked() throws ParseException {
+ try {
+ compileSchema("""
+ search test {
+ document test { }
+ rank-profile p4 {
+ function foobar() {
+ expression: normalize_linear(nativeRank)
+ }
+ first-phase {
+ expression: nativeRank
+ }
+ summary-features {
+ nativeRank
+ foobar
+ }
+ }
+ }
+ """);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Rank profile 'p4' is invalid: " +
+ "Cannot use normalize_linear(nativeRank) from summary-feature foobar, only valid in global-phase expression",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ void requireThatNormalizerInNormalizerIsChecked() throws ParseException {
+ try {
+ compileSchema("""
+ search test {
+ document test {
+ field title type string {
+ indexing: index
+ }
+ }
+ rank-profile p5 {
+ function foobar() {
+ expression: reciprocal_rank(nativeRank)
+ }
+ first-phase {
+ expression: nativeRank
+ }
+ global-phase {
+ expression: normalize_linear(fieldMatch(title)) + normalize_linear(foobar)
+ }
+ }
+ }
+ """);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Rank profile 'p5' is invalid: " +
+ "Cannot use reciprocal_rank(nativeRank) from normalizer input foobar, only valid in global-phase expression",
+ Exceptions.toMessageString(e));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java
index f2e4ec052cb..a38a29893e0 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java
@@ -16,6 +16,7 @@ import com.yahoo.container.logging.ConnectionLogConfig;
import com.yahoo.container.logging.FileConnectionLog;
import com.yahoo.container.logging.JSONAccessLog;
import com.yahoo.container.logging.VespaAccessLog;
+import com.yahoo.jdisc.http.ServerConfig;
import com.yahoo.vespa.model.container.ApplicationContainerCluster;
import com.yahoo.vespa.model.container.component.Component;
import org.junit.jupiter.api.Test;
@@ -129,6 +130,7 @@ public class AccessLogTest extends ContainerModelBuilderTestBase {
assertEquals("default", config.cluster());
assertEquals(-1, config.queueSize());
assertEquals(256 * 1024, config.bufferSize());
+ assertTrue(root.getConfig(ServerConfig.class, "default/container.0/DefaultHttpServer").connectionLog().enabled());
}
@Test
@@ -141,6 +143,7 @@ public class AccessLogTest extends ContainerModelBuilderTestBase {
createModel(root, clusterElem);
Component<?, ?> fileConnectionLogComponent = getComponent("default", FileConnectionLog.class.getName());
assertNull(fileConnectionLogComponent);
+ assertFalse(root.getConfig(ServerConfig.class, "default/container.0/DefaultHttpServer").connectionLog().enabled());
}
@Test
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/HandlerBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/HandlerBuilderTest.java
index 8a7ca27eec5..fdeea85c5a3 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/HandlerBuilderTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/HandlerBuilderTest.java
@@ -1,11 +1,11 @@
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model.container.xml;
+import com.yahoo.config.application.api.DeployLogger;
import com.yahoo.config.model.ConfigModelContext;
import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
import com.yahoo.config.model.deploy.DeployState;
import com.yahoo.config.model.deploy.TestProperties;
-import com.yahoo.config.provision.ApplicationId;
import com.yahoo.container.ComponentsConfig;
import com.yahoo.container.jdisc.JdiscBindingsConfig;
import com.yahoo.container.usability.BindingsOverviewHandler;
@@ -15,8 +15,10 @@ import com.yahoo.vespa.model.container.component.Handler;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Element;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.logging.Level;
import static com.yahoo.vespa.model.container.ContainerCluster.ROOT_HANDLER_BINDING;
import static com.yahoo.vespa.model.container.ContainerCluster.STATE_HANDLER_BINDING_1;
@@ -25,7 +27,11 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
/**
* Tests for container model building with custom handlers.
@@ -63,6 +69,31 @@ public class HandlerBuilderTest extends ContainerModelBuilderTestBase {
}
@Test
+ void warn_on_bindings_shared_by_multiple_handlers() {
+ class TestDeployLogger implements DeployLogger {
+ List<String> logs = new ArrayList<>();
+ @Override public void log(Level level, String message) { logs.add(message); }
+ }
+ var clusterElem = DomBuilderTest.parse(
+ "<container id='default' version='1.0'>",
+ " <handler id='myHandler1'>",
+ " <binding>http://*/myhandler</binding>",
+ " <binding>https://*/myhandler</binding>",
+ " </handler>",
+ " <handler id='myHandler2'>",
+ " <binding>http://*/myhandler</binding>",
+ " <binding>https://*/myhandler</binding>",
+ " </handler>",
+ "</container>");
+ var logger = new TestDeployLogger();
+ createModel(root, logger, clusterElem);
+ assertEquals(
+ List.of("Binding 'http://*/myhandler' was already in use by handler 'myHandler1', but will now be taken over by handler: myHandler2",
+ "Binding 'https://*/myhandler' was already in use by handler 'myHandler1', but will now be taken over by handler: myHandler2"),
+ logger.logs);
+ }
+
+ @Test
void default_root_handler_binding_can_be_stolen_by_user_configured_handler() {
Element clusterElem = DomBuilderTest.parse(
"<container id='default' version='1.0'>" +
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFilesTest.java b/config-model/src/test/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFilesTest.java
index 92fb89a5c4c..b4a54548062 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFilesTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/filedistribution/UserConfiguredFilesTest.java
@@ -5,12 +5,15 @@ import com.yahoo.config.FileNode;
import com.yahoo.config.FileReference;
import com.yahoo.config.ModelReference;
import com.yahoo.config.UrlReference;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
import com.yahoo.config.application.api.FileRegistry;
import com.yahoo.config.model.application.provider.BaseDeployLogger;
import com.yahoo.config.model.deploy.TestProperties;
import com.yahoo.config.model.producer.UserConfigRepo;
import com.yahoo.config.model.test.MockApplicationPackage;
import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.schema.processing.ReservedRankingExpressionFunctionNamesTestCase;
import com.yahoo.vespa.config.ConfigDefinition;
import com.yahoo.vespa.config.ConfigDefinitionKey;
import com.yahoo.vespa.config.ConfigPayloadBuilder;
@@ -27,6 +30,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.logging.Level;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -72,22 +76,19 @@ public class UserConfiguredFilesTest {
}
private UserConfiguredFiles userConfiguredFiles() {
- return new UserConfiguredFiles(fileRegistry,
- new BaseDeployLogger(),
- new TestProperties(),
- new ApplicationContainerCluster.UserConfiguredUrls(),
- new MockApplicationPackage.Builder().build());
+ return userConfiguredFiles(new MockApplicationPackage.Builder().build());
+ }
+
+ private UserConfiguredFiles userConfiguredFiles(ApplicationPackage applicationPackage) {
+ return userConfiguredFiles(applicationPackage, new BaseDeployLogger());
}
- private UserConfiguredFiles userConfiguredFiles(File root, com.yahoo.path.Path path) {
+ private UserConfiguredFiles userConfiguredFiles(ApplicationPackage applicationPackage, DeployLogger deployLogger) {
return new UserConfiguredFiles(fileRegistry,
- new BaseDeployLogger(),
+ deployLogger,
new TestProperties(),
new ApplicationContainerCluster.UserConfiguredUrls(),
- new MockApplicationPackage.Builder()
- .withRoot(root)
- .withFiles(Map.of(path, ""))
- .build());
+ applicationPackage);
}
@BeforeEach
@@ -304,17 +305,18 @@ public class UserConfiguredFilesTest {
@Test
void require_that_using_empty_dir_fails(@TempDir Path tempDir) {
String relativeTempDir = tempDir.toString().substring(tempDir.toString().lastIndexOf("target") + 7);
- try {
- def.addPathDef("pathVal");
- builder.setField("pathVal", relativeTempDir);
- fileRegistry.pathToRef.put(relativeTempDir, new FileReference("bazshash"));
- userConfiguredFiles(tempDir.toFile().getParentFile(),
- com.yahoo.path.Path.fromString(tempDir.toFile().getAbsolutePath())).register(producer);
- fail("Should have thrown exception");
- } catch (IllegalArgumentException e) {
- assertEquals("Invalid config in services.xml for 'mynamespace.myname': Directory '" + relativeTempDir + "' is empty",
- e.getMessage());
- }
+ ApplicationPackage applicationPackage =
+ new MockApplicationPackage.Builder()
+ .withRoot(tempDir.toFile().getParentFile())
+ .withFiles(Map.of(com.yahoo.path.Path.fromString(tempDir.toFile().getAbsolutePath()), ""))
+ .build();
+
+ var logger = new TestDeployLogger();
+ def.addPathDef("pathVal");
+ builder.setField("pathVal", relativeTempDir);
+ fileRegistry.pathToRef.put(relativeTempDir, new FileReference("bazshash"));
+ userConfiguredFiles(applicationPackage, logger).register(producer);
+ assertEquals("Directory '" + relativeTempDir + "' is empty", logger.log);
}
@Test
@@ -331,4 +333,12 @@ public class UserConfiguredFilesTest {
}
}
+ private static class TestDeployLogger implements DeployLogger {
+ public String log = "";
+ @Override
+ public void log(Level level, String message) {
+ log += message;
+ }
+ }
+
}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
index ed0f9aac884..d0b4ad9e917 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
@@ -2,8 +2,6 @@
package com.yahoo.config.provision;
import com.yahoo.component.Version;
-import com.yahoo.config.provision.ZoneEndpoint.AccessType;
-import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
import java.util.Objects;
import java.util.Optional;
@@ -79,6 +77,7 @@ public final class ClusterSpec {
return combinedId;
}
+
/**
* Returns whether the physical hosts running the nodes of this application can
* also run nodes of other applications. Using exclusive nodes for containers increases security and cost.
@@ -96,12 +95,6 @@ public final class ClusterSpec {
return new ClusterSpec(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, zoneEndpoint, stateful);
}
- // TODO: Remove after July 2023
- @Deprecated
- public ClusterSpec exclusive(boolean exclusive) {
- return new ClusterSpec(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, zoneEndpoint, stateful);
- }
-
/** Creates a ClusterSpec when requesting a cluster */
public static Builder request(Type type, Id id) {
return new Builder(type, id);
@@ -121,6 +114,7 @@ public final class ClusterSpec {
private Optional<DockerImage> dockerImageRepo = Optional.empty();
private Version vespaVersion;
private boolean exclusive = false;
+ private boolean provisionForApplication = false;
private Optional<Id> combinedId = Optional.empty();
private ZoneEndpoint zoneEndpoint = ZoneEndpoint.defaultEndpoint;
private boolean stateful;
@@ -155,6 +149,11 @@ public final class ClusterSpec {
return this;
}
+ public Builder provisionForApplication(boolean provisionForApplication) {
+ this.provisionForApplication = provisionForApplication;
+ return this;
+ }
+
public Builder combinedId(Optional<Id> combinedId) {
this.combinedId = combinedId;
return this;
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
index 2680b4babb1..3de9d5aef4b 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
@@ -660,11 +660,14 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
log.log(Level.FINE, () -> "Remove unused file references last modified before " + instant);
List<String> fileReferencesToDelete = sortedUnusedFileReferences(fileDirectory.getRoot(), fileReferencesInUse, instant);
- if (fileReferencesToDelete.size() > 0) {
- log.log(Level.FINE, () -> "Will delete file references not in use: " + fileReferencesToDelete);
- fileReferencesToDelete.forEach(fileReference -> fileDirectory.delete(new FileReference(fileReference), this::isFileReferenceInUse));
+ // Do max 20 at a time
+ var toDelete = fileReferencesToDelete.subList(0, Math.min(fileReferencesToDelete.size(), 20));
+ if (toDelete.size() > 0) {
+ log.log(Level.FINE, () -> "Will delete file references not in use: " + toDelete);
+ toDelete.forEach(fileReference -> fileDirectory.delete(new FileReference(fileReference), this::isFileReferenceInUse));
+ log.log(Level.FINE, () -> "Deleted " + toDelete.size() + " file references not in use");
}
- return fileReferencesToDelete;
+ return toDelete;
}
private boolean isFileReferenceInUse(FileReference fileReference) {
diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java b/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java
index 4c5ac951384..d94244b0e47 100644
--- a/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java
+++ b/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java
@@ -109,6 +109,10 @@ public class DataplaneProxyService extends AbstractComponent {
config.tokenPort(),
config.tokenEndpoints(),
root));
+ if (configChanged) {
+ logger.log(Level.INFO, "Configuring data plane proxy service. Token endpoints: [%s]"
+ .formatted(String.join(", ", config.tokenEndpoints())));
+ }
if (configChanged && state == NginxState.RUNNING) {
changeState(NginxState.RELOAD_REQUIRED);
}
diff --git a/container-search/src/main/java/com/yahoo/search/ranking/GlobalPhaseRanker.java b/container-search/src/main/java/com/yahoo/search/ranking/GlobalPhaseRanker.java
index bb5a991c304..829d0c268e5 100644
--- a/container-search/src/main/java/com/yahoo/search/ranking/GlobalPhaseRanker.java
+++ b/container-search/src/main/java/com/yahoo/search/ranking/GlobalPhaseRanker.java
@@ -50,9 +50,7 @@ public class GlobalPhaseRanker {
return Optional.empty();
}
- public void rerankHits(Query query, Result result, String schema) {
- var setup = globalPhaseSetupFor(query, schema).orElse(null);
- if (setup == null) return;
+ static void rerankHitsImpl(GlobalPhaseSetup setup, Query query, Result result) {
var mainSpec = setup.globalPhaseEvalSpec;
var mainSrc = withQueryPrep(mainSpec.evalSource(), mainSpec.fromQuery(), query);
int rerankCount = resolveRerankCount(setup, query);
@@ -68,6 +66,13 @@ public class GlobalPhaseRanker {
hideImplicitMatchFeatures(result, setup.matchFeaturesToHide);
}
+ public void rerankHits(Query query, Result result, String schema) {
+ var setup = globalPhaseSetupFor(query, schema);
+ if (setup.isPresent()) {
+ rerankHitsImpl(setup.get(), query, result);
+ }
+ }
+
static Supplier<Evaluator> withQueryPrep(Supplier<Evaluator> evalSource, List<String> queryFeatures, Query query) {
var prepared = PreparedInput.findFromQuery(query, queryFeatures);
Supplier<Evaluator> supplier = () -> {
@@ -80,7 +85,7 @@ public class GlobalPhaseRanker {
return supplier;
}
- private void hideImplicitMatchFeatures(Result result, Collection<String> namesToHide) {
+ private static void hideImplicitMatchFeatures(Result result, Collection<String> namesToHide) {
if (namesToHide.size() == 0) return;
var filter = new MatchFeatureFilter(namesToHide);
for (var iterator = result.hits().deepIterator(); iterator.hasNext();) {
@@ -94,7 +99,7 @@ public class GlobalPhaseRanker {
if (newValue.fieldCount() == 0) {
hit.removeField("matchfeatures");
} else {
- hit.setField("matchfeatures", newValue);
+ hit.setField("matchfeatures", new FeatureData(newValue));
}
}
}
@@ -106,7 +111,7 @@ public class GlobalPhaseRanker {
.flatMap(evaluator -> evaluator.getGlobalPhaseSetup(query.getRanking().getProfile()));
}
- private int resolveRerankCount(GlobalPhaseSetup setup, Query query) {
+ private static int resolveRerankCount(GlobalPhaseSetup setup, Query query) {
if (setup == null) {
// there is no global-phase at all (ignore override)
return 0;
diff --git a/container-search/src/main/java/com/yahoo/search/ranking/PreparedInput.java b/container-search/src/main/java/com/yahoo/search/ranking/PreparedInput.java
index 5ab2d7160f9..346acccd916 100644
--- a/container-search/src/main/java/com/yahoo/search/ranking/PreparedInput.java
+++ b/container-search/src/main/java/com/yahoo/search/ranking/PreparedInput.java
@@ -30,7 +30,7 @@ record PreparedInput(String name, Tensor value) {
for (String queryFeatureName : queryFeatures) {
String needed = "query(" + queryFeatureName + ")";
// searchers are recommended to place query features here:
- var feature = rankFeatures.getTensor(queryFeatureName);
+ var feature = rankFeatures.getTensor(needed);
if (feature.isPresent()) {
result.add(new PreparedInput(needed, feature.get()));
} else {
@@ -38,6 +38,8 @@ record PreparedInput(String name, Tensor value) {
var objList = rankProps.get(queryFeatureName);
if (objList != null && objList.size() == 1 && objList.get(0) instanceof Tensor t) {
result.add(new PreparedInput(needed, t));
+ } else if (objList != null && objList.size() == 1 && objList.get(0) instanceof Double d) {
+ result.add(new PreparedInput(needed, Tensor.from(d)));
} else {
throw new IllegalArgumentException("missing query feature: " + queryFeatureName);
}
diff --git a/container-search/src/test/java/com/yahoo/search/ranking/GlobalPhaseRerankHitsImplTest.java b/container-search/src/test/java/com/yahoo/search/ranking/GlobalPhaseRerankHitsImplTest.java
new file mode 100644
index 00000000000..ce9ac377908
--- /dev/null
+++ b/container-search/src/test/java/com/yahoo/search/ranking/GlobalPhaseRerankHitsImplTest.java
@@ -0,0 +1,238 @@
+// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.ranking;
+
+import com.yahoo.data.access.Inspectable;
+import com.yahoo.data.access.Type;
+import com.yahoo.data.access.helpers.MatchFeatureData;
+import com.yahoo.data.access.simple.Value;
+import com.yahoo.search.Query;
+import com.yahoo.search.Result;
+import com.yahoo.search.result.FeatureData;
+import com.yahoo.search.result.Hit;
+import com.yahoo.tensor.Tensor;
+import org.junit.jupiter.api.Test;
+
+import java.util.*;
+import java.util.function.Supplier;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class GlobalPhaseRerankHitsImplTest {
+ static class EvalSum implements Evaluator {
+ double baseValue;
+ List<Tensor> values = new ArrayList<>();
+ EvalSum(double baseValue) { this.baseValue = baseValue; }
+ @Override public Evaluator bind(String name, Tensor value) {
+ values.add(value);
+ return this;
+ }
+ @Override public double evaluateScore() {
+ double result = baseValue;
+ for (var value: values) {
+ result += value.asDouble();
+ }
+ return result;
+ }
+ }
+ static FunEvalSpec makeConstSpec(double constValue) {
+ return new FunEvalSpec(() -> new EvalSum(constValue), Collections.emptyList(), Collections.emptyList());
+ }
+ static FunEvalSpec makeSumSpec(List<String> fromQuery, List<String> fromMF) {
+ return new FunEvalSpec(() -> new EvalSum(0.0), fromQuery, fromMF);
+ }
+ static class ExpectingNormalizer extends Normalizer {
+ List<Double> expected;
+ ExpectingNormalizer(List<Double> expected) {
+ super(100);
+ this.expected = expected;
+ }
+ @Override void normalize() {
+ double rank = 1;
+ assertEquals(size, expected.size());
+ for (int i = 0; i < size; i++) {
+ assertEquals(data[i], expected.get(i));
+ data[i] = rank;
+ rank += 1;
+ }
+ }
+ @Override String normalizing() { return "expecting"; }
+ }
+ static NormalizerSetup makeNormalizer(String name, List<Double> expected, FunEvalSpec evalSpec) {
+ return new NormalizerSetup(name, () -> new ExpectingNormalizer(expected), evalSpec);
+ }
+ static GlobalPhaseSetup makeFullSetup(FunEvalSpec mainSpec, int rerankCount,
+ List<String> hiddenMF, List<NormalizerSetup> normalizers)
+ {
+ return new GlobalPhaseSetup(mainSpec, rerankCount, hiddenMF, normalizers);
+ }
+ static GlobalPhaseSetup makeSimpleSetup(FunEvalSpec mainSpec, int rerankCount) {
+ return makeFullSetup(mainSpec, rerankCount, Collections.emptyList(), Collections.emptyList());
+ }
+ static GlobalPhaseSetup makeNormSetup(FunEvalSpec mainSpec, List<NormalizerSetup> normalizers) {
+ return makeFullSetup(mainSpec, 100, Collections.emptyList(), normalizers);
+ }
+ static record NamedValue(String name, double value) {}
+ NamedValue value(String name, double value) {
+ return new NamedValue(name, value);
+ }
+ Query makeQuery(List<NamedValue> inQuery, boolean withPrepare) {
+ var query = new Query();
+ for (var v: inQuery) {
+ query.getRanking().getFeatures().put(v.name, v.value);
+ }
+ if (withPrepare) {
+ query.getRanking().prepare();
+ }
+ return query;
+ }
+ Query makeQuery(List<NamedValue> inQuery) { return makeQuery(inQuery, false); }
+ Query makeQueryWithPrepare(List<NamedValue> inQuery) { return makeQuery(inQuery, true); }
+
+ static Hit makeHit(String id, double score, FeatureData mf) {
+ Hit hit = new Hit(id, score);
+ hit.setField("matchfeatures", mf);
+ return hit;
+ }
+ static Hit hit(String id, double score) {
+ return makeHit(id, score, FeatureData.empty());
+ }
+ static class HitFactory {
+ MatchFeatureData mfData;
+ Map<String,Integer> map = new HashMap<>();
+ HitFactory(List<String> mfNames) {
+ int i = 0;
+ for (var name: mfNames) {
+ map.put(name, i++);
+ }
+ mfData = new MatchFeatureData(mfNames);
+ }
+ Hit create(String id, double score, List<NamedValue> inMF) {
+ var mf = mfData.addHit();
+ for (var v: inMF) {
+ var idx = map.get(v.name);
+ assertNotNull(idx);
+ mf.set(idx, v.value);
+ }
+ return makeHit(id, score, new FeatureData(mf));
+ }
+ }
+ Result makeResult(Query query, List<Hit> hits) {
+ var result = new Result(query);
+ result.hits().addAll(hits);
+ return result;
+ }
+ static class Expect {
+ Map<String,Double> map = new HashMap<>();
+ static Expect make(List<Hit> hits) {
+ var result = new Expect();
+ for (var hit : hits) {
+ result.map.put(hit.getId().stringValue(), hit.getRelevance().getScore());
+ }
+ return result;
+ }
+ void verifyScores(Result actual) {
+ double prev = Double.MAX_VALUE;
+ assertEquals(actual.hits().size(), map.size());
+ for (var hit : actual.hits()) {
+ var name = hit.getId().stringValue();
+ var score = map.get(name);
+ assertNotNull(score, name);
+ assertEquals(score.doubleValue(), hit.getRelevance().getScore(), name);
+ assertTrue(score <= prev);
+ prev = score;
+ }
+ }
+ }
+ void verifyHasMF(Result result, String name) {
+ for (var hit: result.hits()) {
+ if (hit.getField("matchfeatures") instanceof FeatureData mf) {
+ assertNotNull(mf.getTensor(name));
+ } else {
+ fail("matchfeatures are missing");
+ }
+ }
+ }
+ void verifyDoesNotHaveMF(Result result, String name) {
+ for (var hit: result.hits()) {
+ if (hit.getField("matchfeatures") instanceof FeatureData mf) {
+ assertNull(mf.getTensor(name));
+ } else {
+ fail("matchfeatures are missing");
+ }
+ }
+ }
+ void verifyDoesNotHaveMatchFeaturesField(Result result) {
+ for (var hit: result.hits()) {
+ assertNull(hit.getField("matchfeatures"));
+ }
+ }
+ @Test void partialRerankWithRescaling() {
+ var setup = makeSimpleSetup(makeConstSpec(3.0), 2);
+ var query = makeQuery(Collections.emptyList());
+ var result = makeResult(query, List.of(hit("a", 3), hit("b", 4), hit("c", 5), hit("d", 6)));
+ var expect = Expect.make(List.of(hit("a", 1), hit("b", 2), hit("c", 3), hit("d", 3)));
+ GlobalPhaseRanker.rerankHitsImpl(setup, query, result);
+ expect.verifyScores(result);
+ }
+ @Test void matchFeaturesCanBePartiallyHidden() {
+ var setup = makeFullSetup(makeSumSpec(Collections.emptyList(), List.of("public_value", "private_value")), 2,
+ List.of("private_value"), Collections.emptyList());
+ var query = makeQuery(Collections.emptyList());
+ var factory = new HitFactory(List.of("public_value", "private_value"));
+ var result = makeResult(query, List.of(factory.create("a", 1, List.of(value("public_value", 2), value("private_value", 3))),
+ factory.create("b", 2, List.of(value("public_value", 5), value("private_value", 7)))));
+ var expect = Expect.make(List.of(hit("a", 5), hit("b", 12)));
+ GlobalPhaseRanker.rerankHitsImpl(setup, query, result);
+ expect.verifyScores(result);
+ verifyHasMF(result, "public_value");
+ verifyDoesNotHaveMF(result, "private_value");
+ }
+ @Test void matchFeaturesCanBeRemoved() {
+ var setup = makeFullSetup(makeSumSpec(Collections.emptyList(), List.of("private_value")), 2,
+ List.of("private_value"), Collections.emptyList());
+ var query = makeQuery(Collections.emptyList());
+ var factory = new HitFactory(List.of("private_value"));
+ var result = makeResult(query, List.of(factory.create("a", 1, List.of(value("private_value", 3))),
+ factory.create("b", 2, List.of(value("private_value", 7)))));
+ var expect = Expect.make(List.of(hit("a", 3), hit("b", 7)));
+ GlobalPhaseRanker.rerankHitsImpl(setup, query, result);
+ expect.verifyScores(result);
+ verifyDoesNotHaveMatchFeaturesField(result);
+ }
+ @Test void queryFeaturesCanBeUsed() {
+ var setup = makeSimpleSetup(makeSumSpec(List.of("foo"), List.of("bar")), 2);
+ var query = makeQuery(List.of(value("query(foo)", 7)));
+ var factory = new HitFactory(List.of("bar"));
+ var result = makeResult(query, List.of(factory.create("a", 1, List.of(value("bar", 2))),
+ factory.create("b", 2, List.of(value("bar", 5)))));
+ var expect = Expect.make(List.of(hit("a", 9), hit("b", 12)));
+ GlobalPhaseRanker.rerankHitsImpl(setup, query, result);
+ expect.verifyScores(result);
+ verifyHasMF(result, "bar");
+ }
+ @Test void queryFeaturesCanBeUsedWhenPrepared() {
+ var setup = makeSimpleSetup(makeSumSpec(List.of("foo"), List.of("bar")), 2);
+ var query = makeQueryWithPrepare(List.of(value("query(foo)", 7)));
+ var factory = new HitFactory(List.of("bar"));
+ var result = makeResult(query, List.of(factory.create("a", 1, List.of(value("bar", 2))),
+ factory.create("b", 2, List.of(value("bar", 5)))));
+ var expect = Expect.make(List.of(hit("a", 9), hit("b", 12)));
+ GlobalPhaseRanker.rerankHitsImpl(setup, query, result);
+ expect.verifyScores(result);
+ verifyHasMF(result, "bar");
+ }
+ @Test void withNormalizer() {
+ var setup = makeNormSetup(makeSumSpec(Collections.emptyList(), List.of("bar")),
+ List.of(makeNormalizer("foo", List.of(115.0, 65.0, 55.0, 45.0, 15.0), makeSumSpec(List.of("x"), List.of("bar")))));
+ var query = makeQuery(List.of(value("query(x)", 5)));
+ var factory = new HitFactory(List.of("bar"));
+ var result = makeResult(query, List.of(factory.create("a", 1, List.of(value("bar", 10))),
+ factory.create("b", 2, List.of(value("bar", 40))),
+ factory.create("c", 3, List.of(value("bar", 50))),
+ factory.create("d", 4, List.of(value("bar", 60))),
+ factory.create("e", 5, List.of(value("bar", 110)))));
+ var expect = Expect.make(List.of(hit("a", 15), hit("b", 44), hit("c", 53), hit("d", 62), hit("e", 111)));
+ GlobalPhaseRanker.rerankHitsImpl(setup, query, result);
+ expect.verifyScores(result);
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java
index ee0df3adbfb..b451df87727 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java
@@ -13,7 +13,6 @@ import java.util.List;
import static com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo.SupportLevel.BASIC;
import static java.math.BigDecimal.ZERO;
-import static java.math.BigDecimal.valueOf;
public class MockPricingController implements PricingController {
@@ -23,34 +22,35 @@ public class MockPricingController implements PricingController {
@Override
public Prices priceForApplications(List<ApplicationResources> applicationResources, PricingInfo pricingInfo, Plan plan) {
- ApplicationResources resources = applicationResources.get(0);
-
- BigDecimal listPrice = resources.vcpu().multiply(cpuCost)
- .add(resources.memoryGb().multiply(memoryCost)
- .add(resources.diskGb().multiply(diskCost))
- .add(resources.enclaveVcpu().multiply(cpuCost)
- .add(resources.enclaveMemoryGb().multiply(memoryCost))
- .add(resources.enclaveDiskGb().multiply(diskCost))));
+ List<PriceInformation> appPrices = applicationResources.stream()
+ .map(resources -> {
+ BigDecimal listPrice = resources.vcpu().multiply(cpuCost)
+ .add(resources.memoryGb().multiply(memoryCost))
+ .add(resources.diskGb().multiply(diskCost))
+ .add(resources.enclaveVcpu().multiply(cpuCost))
+ .add(resources.enclaveMemoryGb().multiply(memoryCost))
+ .add(resources.enclaveDiskGb().multiply(diskCost));
- BigDecimal supportLevelCost = pricingInfo.supportLevel() == BASIC ? new BigDecimal("-1.00") : new BigDecimal("8.00");
- BigDecimal listPriceWithSupport = listPrice.add(supportLevelCost);
- BigDecimal enclaveDiscount = isEnclave(resources) ? new BigDecimal("-0.15") : BigDecimal.ZERO;
- BigDecimal volumeDiscount = new BigDecimal("-0.1");
- BigDecimal appTotalAmount = listPrice.add(supportLevelCost).add(enclaveDiscount).add(volumeDiscount);
+ BigDecimal supportLevelCost = pricingInfo.supportLevel() == BASIC ? new BigDecimal("-1.00") : new BigDecimal("8.00");
+ BigDecimal listPriceWithSupport = listPrice.add(supportLevelCost);
+ BigDecimal enclaveDiscount = isEnclave(resources) ? new BigDecimal("-0.15") : BigDecimal.ZERO;
+ BigDecimal volumeDiscount = new BigDecimal("-0.1");
+ BigDecimal appTotalAmount = listPrice.add(supportLevelCost).add(enclaveDiscount).add(volumeDiscount);
- List<PriceInformation> appPrices = applicationResources.stream()
- .map(appResources -> new PriceInformation(listPriceWithSupport,
- volumeDiscount,
- ZERO,
- enclaveDiscount,
- appTotalAmount))
+ return new PriceInformation(listPriceWithSupport,
+ volumeDiscount,
+ ZERO,
+ enclaveDiscount,
+ appTotalAmount);
+ })
.toList();
PriceInformation sum = PriceInformation.sum(appPrices);
- var committedAmountDiscount = new BigDecimal("-0.2");
+ System.out.println(pricingInfo.committedHourlyAmount());
+ var committedAmountDiscount = pricingInfo.committedHourlyAmount().compareTo(ZERO) > 0 ? new BigDecimal("-0.2") : ZERO;
var totalAmount = sum.totalAmount().add(committedAmountDiscount);
var enclave = ZERO;
- if (resources.enclave() && totalAmount.compareTo(new BigDecimal("14.00")) < 0)
+ if (applicationResources.stream().anyMatch(ApplicationResources::enclave) && totalAmount.compareTo(new BigDecimal("14.00")) < 0)
enclave = new BigDecimal("14.00").subtract(totalAmount);
var totalPrice = new PriceInformation(ZERO, ZERO, committedAmountDiscount, enclave, totalAmount);
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
index 4a61ff30c25..9ceeba32061 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
@@ -51,11 +51,16 @@ public class CloudTenant extends Tenant {
/** Creates a tenant with the given name, provided it passes validation. */
public static CloudTenant create(TenantName tenantName, Instant createdAt, Principal creator) {
+ // Initialize with creator as verified contact
+ var info = TenantInfo.empty().withContacts(new TenantContacts(List.of(
+ new TenantContacts.EmailContact(
+ List.of(TenantContacts.Audience.TENANT, TenantContacts.Audience.NOTIFICATIONS),
+ new Email(creator.getName(), true)))));
return new CloudTenant(requireName(tenantName),
createdAt,
LastLoginInfo.EMPTY,
Optional.ofNullable(creator).map(SimplePrincipal::of),
- ImmutableBiMap.of(), TenantInfo.empty(), List.of(), new ArchiveAccess(), Optional.empty(),
+ ImmutableBiMap.of(), info, List.of(), new ArchiveAccess(), Optional.empty(),
Instant.EPOCH, List.of(), Optional.empty(), PlanId.from("none"));
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
index f9798fb2559..f11d67762ad 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
@@ -64,6 +64,8 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -79,6 +81,8 @@ import static java.util.stream.Collectors.toMap;
*/
public class RoutingController {
+ private static final Logger LOG = Logger.getLogger(RoutingController.class.getName());
+
private final Controller controller;
private final RoutingPolicies routingPolicies;
private final RotationRepository rotationRepository;
@@ -213,6 +217,8 @@ public class RoutingController {
// Register rotation-backed endpoints in DNS
registerRotationEndpointsInDns(prepared);
+ LOG.log(Level.FINE, () -> "Prepared endpoints: " + prepared);
+
return prepared;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java
index 7fa2a03d0a9..c2949e395e9 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java
@@ -23,16 +23,6 @@ public record AssignedRotation(ClusterSpec.Id clusterId, EndpointId endpointId,
this.regions = Set.copyOf(Objects.requireNonNull(regions));
}
- @Override
- public String toString() {
- return "AssignedRotation{" +
- "clusterId=" + clusterId +
- ", endpointId='" + endpointId + '\'' +
- ", rotationId=" + rotationId +
- ", regions=" + regions +
- '}';
- }
-
private static <T> T requireNonEmpty(T object, String value, String field) {
Objects.requireNonNull(object);
Objects.requireNonNull(value);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
index fcb64b4c00f..d46f59f36a7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
@@ -36,7 +36,6 @@ import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.S
/**
* Expires unused tenants from Vespa Cloud.
- * <p>
*
* @author ogronnesby
*/
@@ -165,6 +164,8 @@ public class CloudTrialExpirer extends ControllerMaintainer {
.subject(emailSubject)
.with("mailMessageTemplate", "cloud-trial-notification")
.with("cloudTrialMessage", emailMsg)
+ .with("mailTitle", emailSubject)
+ .with("consoleLink", controller().zoneRegistry().dashboardUrl(tenant.name()).toString())
.build());
var source = NotificationSource.from(tenant.name());
// Remove previous notification to ensure new notification is sent by email
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
index 40c24c6f339..5116ecaf053 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
@@ -2,11 +2,15 @@
package com.yahoo.vespa.hosted.controller.notification;
import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.SortedMap;
+import java.util.TreeMap;
/**
* Represents an event that we want to notify the tenant about. The message(s) should be short
@@ -78,34 +82,57 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve
public static class MailContent {
private final String template;
- private final Map<String, Object> values;
+ private final SortedMap<String, Object> values;
private final String subject;
private MailContent(Builder b) {
template = Objects.requireNonNull(b.template);
- values = Map.copyOf(b.values);
+ values = new TreeMap<>(b.values);
subject = b.subject;
}
public String template() { return template; }
- public Map<String, Object> values() { return Map.copyOf(values); }
+ public SortedMap<String, Object> values() { return Collections.unmodifiableSortedMap(values); }
public Optional<String> subject() { return Optional.ofNullable(subject); }
public static Builder fromTemplate(String template) { return new Builder(template); }
public static class Builder {
private final String template;
- private final HashMap<String, Object> values = new HashMap<>();
+ private final Map<String, Object> values = new HashMap<>();
private String subject;
private Builder(String template) {
this.template = template;
}
- public Builder with(String name, Object value) { values.put(name, value); return this; }
+ public Builder with(String name, String value) { values.put(name, value); return this; }
+ public Builder with(String name, Collection<String> items) { values.put(name, List.copyOf(items)); return this; }
public Builder subject(String s) { this.subject = s; return this; }
public MailContent build() { return new MailContent(this); }
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ MailContent that = (MailContent) o;
+ return Objects.equals(template, that.template) && Objects.equals(values, that.values) && Objects.equals(subject, that.subject);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(template, values, subject);
+ }
+
+ @Override
+ public String toString() {
+ return "MailContent{" +
+ "template='" + template + '\'' +
+ ", values=" + values +
+ ", subject='" + subject + '\'' +
+ '}';
+ }
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
index e752e13eddd..a5d26feafaa 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
@@ -71,7 +71,6 @@ public class NotificationsDb {
/**
* Add a notification with given source and type. If a notification with same source and type
* already exists, it'll be replaced by this one instead.
- * Email content is not persisted here. The email dispatcher is responsible for reliable delivery.
*/
public void setNotification(NotificationSource source, Type type, Level level, List<String> messages,
Optional<MailContent> mailContent) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
index 7915a833be6..62b35d4cfd4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
@@ -8,6 +8,7 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
+import com.yahoo.slime.ObjectTraverser;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
@@ -15,6 +16,7 @@ import com.yahoo.vespa.hosted.controller.notification.Notification;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import java.util.List;
+import java.util.Optional;
/**
* (de)serializes notifications for a tenant
@@ -60,6 +62,22 @@ public class NotificationsSerializer {
notification.source().clusterId().ifPresent(clusterId -> notificationObject.setString(clusterIdField, clusterId.value()));
notification.source().jobType().ifPresent(jobType -> notificationObject.setString(jobTypeField, jobType.serialized()));
notification.source().runNumber().ifPresent(runNumber -> notificationObject.setLong(runNumberField, runNumber));
+
+ notification.mailContent().ifPresent(mc -> {
+ notificationObject.setString("mail-template", mc.template());
+ mc.subject().ifPresent(s -> notificationObject.setString("mail-subject", s));
+ var mailParamsCursor = notificationObject.setObject("mail-params");
+ mc.values().forEach((key, value) -> {
+ if (value instanceof String str) {
+ mailParamsCursor.setString(key, str);
+ } else if (value instanceof List<?> l) {
+ var array = mailParamsCursor.setArray(key);
+ l.forEach(elem -> array.addString((String) elem));
+ } else {
+ throw new ClassCastException("Unsupported param type: " + value.getClass());
+ }
+ });
+ });
}
return slime;
@@ -92,7 +110,24 @@ public class NotificationsSerializer {
SlimeUtils.optionalString(inspector.field(clusterIdField)).map(ClusterSpec.Id::from),
SlimeUtils.optionalString(inspector.field(jobTypeField)).map(jobName -> JobType.ofSerialized(jobName)),
SlimeUtils.optionalLong(inspector.field(runNumberField))),
- SlimeUtils.entriesStream(inspector.field(messagesField)).map(Inspector::asString).toList());
+ SlimeUtils.entriesStream(inspector.field(messagesField)).map(Inspector::asString).toList(),
+ mailContentFrom(inspector));
+ }
+
+ private Optional<Notification.MailContent> mailContentFrom(final Inspector inspector) {
+ return SlimeUtils.optionalString(inspector.field("mail-template")).map(template -> {
+ var builder = Notification.MailContent.fromTemplate(template);
+ SlimeUtils.optionalString(inspector.field("mail-subject")).ifPresent(builder::subject);
+ var paramsCursor = inspector.field("mail-params");
+ inspector.field("mail-params").traverse((ObjectTraverser) (name, insp) -> {
+ switch (insp.type()) {
+ case STRING -> builder.with(name, insp.asString());
+ case ARRAY -> builder.with(name, SlimeUtils.entriesStream(insp).map(Inspector::asString).toList());
+ default -> throw new IllegalArgumentException("Unsupported param type: " + insp.type());
+ }
+ });
+ return builder.build();
+ });
}
private static String asString(Notification.Type type) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index d7fb09cdf73..fdde87074e9 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -705,7 +705,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
var contact = info.billingContact().contact();
var address = info.billingContact().address();
- var mergedContact = updateTenantInfoContact(inspector.field("contact"), cloudTenant.name(), contact, false);
+ var mergedContact = updateBillingContact(inspector.field("contact"), cloudTenant.name(), contact);
var mergedAddress = updateTenantInfoAddress(inspector.field("address"), info.billingContact().address());
var mergedBilling = info.billingContact()
@@ -893,15 +893,13 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
throw new IllegalArgumentException("All address fields must be set");
}
- private TenantContact updateTenantInfoContact(Inspector insp, TenantName tenantName, TenantContact oldContact, boolean isBillingContact) {
+ private TenantContact updateBillingContact(Inspector insp, TenantName tenantName, TenantContact oldContact) {
if (!insp.valid()) return oldContact;
var mergedEmail = optional("email", insp)
.filter(address -> !address.equals(oldContact.email().getEmailAddress()))
.map(address -> {
- var mailType = isBillingContact ? PendingMailVerification.MailType.BILLING :
- PendingMailVerification.MailType.TENANT_CONTACT;
- controller.mailVerifier().sendMailVerification(tenantName, address, mailType);
+ controller.mailVerifier().sendMailVerification(tenantName, address, PendingMailVerification.MailType.BILLING);
return new Email(address, false);
})
.orElse(oldContact.email());
@@ -916,7 +914,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
if (!insp.valid()) return oldContact;
return TenantBilling.empty()
- .withContact(updateTenantInfoContact(insp, tenantName, oldContact.contact(), true))
+ .withContact(updateBillingContact(insp, tenantName, oldContact.contact()))
.withAddress(updateTenantInfoAddress(insp.field("address"), oldContact.address()));
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
index 718320c02ca..83cd5dab2f3 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.restapi.billing;
import com.yahoo.config.provision.TenantName;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
+import com.yahoo.messagebus.Message;
import com.yahoo.restapi.MessageResponse;
import com.yahoo.restapi.RestApi;
import com.yahoo.restapi.RestApiException;
@@ -98,6 +99,9 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
.post(Slime.class, self::newAdditionalItem))
.addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/item/{item}")
.delete(self::deleteAdditionalItem))
+ .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/plan")
+ .get(self::accountantTenantPlan)
+ .post(Slime.class, self::setAccountantTenantPlan))
.addRoute(RestApi.route("/billing/v2/accountant/bill/{invoice}/export")
.put(Slime.class, self::putAccountantInvoiceExport))
.addRoute(RestApi.route("/billing/v2/accountant/plans")
@@ -361,6 +365,39 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
return slime;
}
+ private MessageResponse setAccountantTenantPlan(RestApi.RequestContext requestContext, Slime body) {
+ var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
+ var tenant = tenants.require(tenantName, CloudTenant.class);
+
+ var planId = PlanId.from(getInspectorFieldOrThrow(body.get(), "id"));
+ var response = billing.setPlan(tenant.name(), planId, false, true);
+
+ if (response.isSuccess()) {
+ return new MessageResponse("Plan: " + planId.value());
+ } else {
+ throw new RestApiException.BadRequest("Could not change plan: " + response.getErrorMessage());
+ }
+ }
+
+ private Slime accountantTenantPlan(RestApi.RequestContext requestContext) {
+ var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
+ var tenant = tenants.require(tenantName, CloudTenant.class);
+
+ var planId = billing.getPlan(tenant.name());
+ var plan = planRegistry.plan(planId);
+
+ if (plan.isEmpty()) {
+ throw new RestApiException.BadRequest("Plan with ID '" + planId.value() + "' does not exist");
+ }
+
+ var slime = new Slime();
+ var root = slime.setObject();
+ root.setString("id", plan.get().id().value());
+ root.setString("name", plan.get().displayName());
+
+ return slime;
+ }
+
// --------- INVOICE RENDERING ----------
private void invoicesSummaryToSlime(Cursor slime, List<Bill> bills) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java
index 0a43ec599d5..9a2a57359d7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java
@@ -4,7 +4,6 @@ package com.yahoo.vespa.hosted.controller.restapi.pricing;
import com.yahoo.collections.Pair;
import com.yahoo.component.annotation.Inject;
import com.yahoo.config.provision.ClusterResources;
-import com.yahoo.config.provision.NodeResources;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
@@ -35,8 +34,6 @@ import java.util.logging.Logger;
import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
import static com.yahoo.restapi.ErrorResponse.methodNotAllowed;
import static com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo.SupportLevel;
-import static java.lang.Double.parseDouble;
-import static java.lang.Integer.parseInt;
import static java.math.BigDecimal.ZERO;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -116,41 +113,13 @@ public class PricingApiHandler extends ThreadedHttpRequestHandler {
default -> throw new IllegalArgumentException("Unknown query parameter '" + entry.getFirst() + '\'');
}
}
- if (appResources.isEmpty()) throw new IllegalArgumentException("No application resources found in query");
PricingInfo pricingInfo = new PricingInfo(supportLevel, committedSpend);
return new PriceParameters(List.of(), pricingInfo, plan, appResources);
}
- private ClusterResources clusterResources(String resourcesString) {
- List<String> elements = Arrays.stream(resourcesString.split(",")).toList();
-
- var nodes = 0;
- var vcpu = 0d;
- var memoryGb = 0d;
- var diskGb = 0d;
- var gpuMemoryGb = 0d;
-
- for (var element : keysAndValues(elements)) {
- var value = element.getSecond();
- switch (element.getFirst().toLowerCase()) {
- case "nodes" -> nodes = parseInt(value);
- case "vcpu" -> vcpu = parseDouble(value);
- case "memorygb" -> memoryGb = parseDouble(value);
- case "diskgb" -> diskGb = parseDouble(value);
- case "gpumemorygb" -> gpuMemoryGb = parseDouble(value);
- default -> throw new IllegalArgumentException("Unknown resource type '" + element.getFirst() + '\'');
- }
- }
-
- var nodeResources = new NodeResources(vcpu, memoryGb, diskGb, 0); // 0 bandwidth, not used in price calculation
- if (gpuMemoryGb > 0)
- nodeResources = nodeResources.with(new NodeResources.GpuResources(1, gpuMemoryGb));
- return new ClusterResources(nodes, 1, nodeResources);
- }
-
private ApplicationResources applicationResources(String appResourcesString) {
- List<String> elements = Arrays.stream(appResourcesString.split(",")).toList();
+ List<String> elements = List.of(appResourcesString.split(","));
var vcpu = ZERO;
var memoryGb = ZERO;
diff --git a/controller-server/src/main/resources/mail/cloud-trial-notification.vm b/controller-server/src/main/resources/mail/cloud-trial-notification.vm
index 27bc9b1ad1b..c1ba394bf8e 100644
--- a/controller-server/src/main/resources/mail/cloud-trial-notification.vm
+++ b/controller-server/src/main/resources/mail/cloud-trial-notification.vm
@@ -1,3 +1,3 @@
<p>
- $esc.html($cloudTrialMessage):
+ $esc.html($cloudTrialMessage)
</p> \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java
index b595c8a8be3..7e8237606c6 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java
@@ -9,6 +9,7 @@ import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
+import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.notification.Notification;
@@ -17,10 +18,15 @@ import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import org.junit.jupiter.api.Test;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -28,6 +34,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
public class CloudTrialExpirerTest {
+ private static final boolean OVERWRITE_TEST_FILES = false;
+
private final ControllerTester tester = new ControllerTester(SystemName.PublicCd);
private final DeploymentTester deploymentTester = new DeploymentTester(tester);
private final CloudTrialExpirer expirer = new CloudTrialExpirer(tester.controller(), Duration.ofMinutes(5));
@@ -94,30 +102,50 @@ public class CloudTrialExpirerTest {
}
@Test
- void queues_trial_notification_based_on_account_age() {
+ void queues_trial_notification_based_on_account_age() throws IOException {
var clock = (ManualClock)tester.controller().clock();
+ var mailer = (MockMailer) tester.serviceRegistry().mailer();
var tenant = TenantName.from("trial-tenant");
((InMemoryFlagSource) tester.controller().flagSource())
.withBooleanFlag(Flags.CLOUD_TRIAL_NOTIFICATIONS.id(), true);
registerTenant(tenant.value(), "trial", Duration.ZERO);
assertEquals(0.0, expirer.maintain());
assertEquals("Welcome to Vespa Cloud", lastAccountLevelNotificationTitle(tenant));
+ assertLastEmailEquals(mailer, "welcome.html");
clock.advance(Duration.ofDays(7));
assertEquals(0.0, expirer.maintain());
assertEquals("How is your Vespa Cloud trial going?", lastAccountLevelNotificationTitle(tenant));
+ assertLastEmailEquals(mailer, "trial-reminder.html");
clock.advance(Duration.ofDays(5));
assertEquals(0.0, expirer.maintain());
assertEquals("Your Vespa Cloud trial expires in 2 days", lastAccountLevelNotificationTitle(tenant));
+ assertLastEmailEquals(mailer, "trial-expiring-soon.html");
clock.advance(Duration.ofDays(1));
assertEquals(0.0, expirer.maintain());
assertEquals("Your Vespa Cloud trial expires tomorrow", lastAccountLevelNotificationTitle(tenant));
+ assertLastEmailEquals(mailer, "trial-expiring-immediately.html");
clock.advance(Duration.ofDays(2));
assertEquals(0.0, expirer.maintain());
assertEquals("Your Vespa Cloud trial has expired", lastAccountLevelNotificationTitle(tenant));
+ assertLastEmailEquals(mailer, "trial-expired.html");
+ }
+
+ private void assertLastEmailEquals(MockMailer mailer, String expectedContentFile) throws IOException {
+ var mails = mailer.inbox("dev-trial-tenant");
+ assertFalse(mails.isEmpty());
+ var content = mails.get(mails.size() - 1).htmlMessage().orElseThrow();
+ var path = Paths.get("src/test/resources/mail/" + expectedContentFile);
+ if (OVERWRITE_TEST_FILES) {
+ Files.write(path, content.getBytes(),
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
+ } else {
+ var expectedContent = Files.readString(path);
+ assertEquals(expectedContent, content);
+ }
}
private void registerTenant(String tenantName, String plan, Duration timeSinceLastLogin) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java
index 63aa45a5a34..26eb30b6525 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java
@@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
+import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -27,6 +28,8 @@ public class NotificationsSerializerTest {
void serialization_test() throws IOException {
NotificationsSerializer serializer = new NotificationsSerializer();
TenantName tenantName = TenantName.from("tenant1");
+ var mail = Notification.MailContent.fromTemplate("my-template").subject("My mail subject")
+ .with("string-param", "string-value").with("list-param", List.of("elem1", "elem2")).build();
List<Notification> notifications = List.of(
new Notification(Instant.ofEpochSecond(1234),
Notification.Type.applicationPackage,
@@ -37,7 +40,8 @@ public class NotificationsSerializerTest {
Notification.Type.deployment,
Notification.Level.error,
NotificationSource.from(new RunId(ApplicationId.from(tenantName.value(), "app1", "instance1"), DeploymentContext.systemTest, 12)),
- List.of("Failed to deploy: Node allocation failure")));
+ List.of("Failed to deploy: Node allocation failure"),
+ Optional.of(mail)));
Slime serialized = serializer.toSlime(notifications);
assertEquals("{\"notifications\":[" +
@@ -55,7 +59,10 @@ public class NotificationsSerializerTest {
"\"application\":\"app1\"," +
"\"instance\":\"instance1\"," +
"\"jobId\":\"test.us-east-1\"," +
- "\"runNumber\":12" +
+ "\"runNumber\":12," +
+ "\"mail-template\":\"my-template\"," +
+ "\"mail-subject\":\"My mail subject\"," +
+ "\"mail-params\":{\"list-param\":[\"elem1\",\"elem2\"],\"string-param\":\"string-value\"}" +
"}]}", new String(SlimeUtils.toJsonBytes(serialized)));
List<Notification> deserialized = serializer.fromSlime(tenantName, serialized);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
index bf908069c1e..90bd2323cb3 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
@@ -49,7 +49,6 @@ import static com.yahoo.application.container.handler.Request.Method.DELETE;
import static com.yahoo.application.container.handler.Request.Method.GET;
import static com.yahoo.application.container.handler.Request.Method.POST;
import static com.yahoo.application.container.handler.Request.Method.PUT;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -79,7 +78,7 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
void tenant_info_profile() {
var request = request("/application/v4/tenant/scoober/info/profile", GET)
.roles(Set.of(Role.reader(tenantName)));
- tester.assertResponse(request, "{}", 200);
+ tester.assertResponse(request, "{\"contact\":{\"name\":\"\",\"email\":\"\",\"emailVerified\":true},\"tenant\":{\"company\":\"\",\"website\":\"\"}}", 200);
var updateRequest = request("/application/v4/tenant/scoober/info/profile", PUT)
.data("{\"contact\":{\"name\":\"Some Name\",\"email\":\"foo@example.com\"},\"tenant\":{\"company\":\"Scoober, Inc.\",\"website\":\"https://example.com/\"}}")
@@ -101,7 +100,7 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
void tenant_info_billing() {
var request = request("/application/v4/tenant/scoober/info/billing", GET)
.roles(Set.of(Role.reader(tenantName)));
- tester.assertResponse(request, "{}", 200);
+ tester.assertResponse(request, "{\"contact\":{\"name\":\"\",\"email\":\"\",\"phone\":\"\"}}", 200);
var fullAddress = "{\"addressLines\":\"addressLines\",\"postalCodeOrZip\":\"postalCodeOrZip\",\"city\":\"city\",\"stateRegionProvince\":\"stateRegionProvince\",\"country\":\"country\"}";
var fullBillingContact = "{\"contact\":{\"name\":\"name\",\"email\":\"foo@example\",\"phone\":\"phone\"},\"address\":" + fullAddress + "}";
@@ -118,7 +117,7 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
void tenant_info_contacts() {
var request = request("/application/v4/tenant/scoober/info/contacts", GET)
.roles(Set.of(Role.reader(tenantName)));
- tester.assertResponse(request, "{\"contacts\":[]}", 200);
+ tester.assertResponse(request, "{\"contacts\":[{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"developer@scoober\",\"emailVerified\":true}]}", 200);
var fullContacts = "{\"contacts\":[{\"audiences\":[\"tenant\"],\"email\":\"contact1@example.com\",\"emailVerified\":false},{\"audiences\":[\"notifications\"],\"email\":\"contact2@example.com\",\"emailVerified\":false},{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"contact3@example.com\",\"emailVerified\":false}]}";
@@ -134,7 +133,7 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
var infoRequest =
request("/application/v4/tenant/scoober/info", GET)
.roles(Set.of(Role.reader(tenantName)));
- tester.assertResponse(infoRequest, "{}", 200);
+ tester.assertResponse(infoRequest, "{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"contactName\":\"\",\"contactEmail\":\"\",\"contactEmailVerified\":true,\"contacts\":[{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"developer@scoober\",\"emailVerified\":true}]}", 200);
String partialInfo = "{\"contactName\":\"newName\", \"contactEmail\": \"foo@example.com\", \"billingContact\":{\"name\":\"billingName\"}}";
var postPartial =
@@ -189,7 +188,7 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
var infoRequest =
request("/application/v4/tenant/scoober/info", GET)
.roles(Set.of(Role.reader(tenantName)));
- tester.assertResponse(infoRequest, "{}", 200);
+ tester.assertResponse(infoRequest, "{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"contactName\":\"\",\"contactEmail\":\"\",\"contactEmailVerified\":true,\"contacts\":[{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"developer@scoober\",\"emailVerified\":true}]}", 200);
// name needs to be present and not blank
var partialInfoMissingName = "{\"contactName\": \" \"}";
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java
index a2290f1f664..424b8d84472 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java
@@ -185,4 +185,30 @@ public class BillingApiHandlerV2Test extends ControllerContainerCloudTest {
{"message":"Successfully deleted line item line-item-id"}""");
}
}
+
+ @Test
+ void require_current_plan() {
+ {
+ var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/plan")
+ .roles(Role.hostedAccountant());
+ tester.assertResponse(accountantRequest, """
+ {"id":"trial","name":"Free Trial - for testing purposes"}""");
+ }
+
+ {
+ var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/plan", Request.Method.POST)
+ .roles(Role.hostedAccountant())
+ .data("""
+ {"id": "paid"}""");
+ tester.assertResponse(accountantRequest, """
+ {"message":"Plan: paid"}""");
+ }
+
+ {
+ var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/plan")
+ .roles(Role.hostedAccountant());
+ tester.assertResponse(accountantRequest, """
+ {"id":"paid","name":"Paid Plan - for testing purposes"}""");
+ }
+ }
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
index d37097b8068..9477e71af33 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
@@ -71,7 +71,7 @@ public class SignatureFilterTest {
filter = new SignatureFilter(tester.controller());
signer = new RequestSigner(privateKey, id.serializedForm(), tester.clock());
- tester.curator().writeTenant(CloudTenant.create(appId.tenant(), Instant.EPOCH, null));
+ tester.curator().writeTenant(CloudTenant.create(appId.tenant(), Instant.EPOCH, new SimplePrincipal("owner@my-tenant.my-app")));
tester.curator().writeApplication(new Application(appId, tester.clock().instant()));
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java
index c4b5a771725..f2ce0dfeef2 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java
@@ -21,6 +21,12 @@ public class PricingApiHandlerTest extends ControllerContainerCloudTest {
@Test
void testPricingInfoBasic() {
+ tester().assertJsonResponse(request("/pricing/v1/pricing?supportLevel=basic&committedSpend=0"),
+ """
+ { "applications": [ ], "priceInfo": [ ], "totalAmount": "0.00" }
+ """,
+ 200);
+
var request = request("/pricing/v1/pricing?" + urlEncodedPriceInformation1App(BASIC));
tester().assertJsonResponse(request, """
{
@@ -110,10 +116,8 @@ public class PricingApiHandlerTest extends ControllerContainerCloudTest {
]
}
],
- "priceInfo": [
- {"description": "Committed spend", "amount": "-0.20"}
- ],
- "totalAmount": "25.90"
+ "priceInfo": [ ],
+ "totalAmount": "26.10"
}
""",
200);
@@ -128,9 +132,6 @@ public class PricingApiHandlerTest extends ControllerContainerCloudTest {
tester.assertJsonResponse(request("/pricing/v1/pricing?"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Error in query parameter, expected '=' between key and value: ''\"}",
400);
- tester.assertJsonResponse(request("/pricing/v1/pricing?supportLevel=basic&committedSpend=0"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No application resources found in query\"}",
- 400);
tester.assertJsonResponse(request("/pricing/v1/pricing?supportLevel=basic&committedSpend=0&resources"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Error in query parameter, expected '=' between key and value: 'resources'\"}",
400);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json
index 6702eff8dde..1926dcc9f82 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json
@@ -5,5 +5,14 @@
"contactName": "administrator",
"contactEmail": "administrator@tenant",
"contactEmailVerified": true,
- "contacts": [ ]
+ "contacts": [
+ {
+ "audiences": [
+ "tenant",
+ "notifications"
+ ],
+ "email": "administrator@tenant",
+ "emailVerified": true
+ }
+ ]
}
diff --git a/controller-server/src/test/resources/mail/trial-expired.html b/controller-server/src/test/resources/mail/trial-expired.html
new file mode 100644
index 00000000000..4e6fda61b33
--- /dev/null
+++ b/controller-server/src/test/resources/mail/trial-expired.html
@@ -0,0 +1,646 @@
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:v="urn:schemas-microsoft-com:vml"
+ xmlns:o="urn:schemas-microsoft-com:office:office"
+>
+ <head>
+ <title></title>
+ <!--[if !mso]><!-->
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <!--<![endif]-->
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <style type="text/css">
+ #outlook a {
+ padding: 0;
+ }
+
+ body {
+ margin: 0;
+ padding: 0;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ }
+
+ table,
+ td {
+ border-collapse: collapse;
+ mso-table-lspace: 0pt;
+ mso-table-rspace: 0pt;
+ }
+
+ img {
+ border: 0;
+ height: auto;
+ line-height: 100%;
+ outline: none;
+ text-decoration: none;
+ -ms-interpolation-mode: bicubic;
+ }
+
+ p {
+ display: block;
+ margin: 13px 0;
+ }
+ </style>
+ <!--[if mso]>
+ <noscript>
+ <xml>
+ <o:OfficeDocumentSettings>
+ <o:AllowPNG />
+ <o:PixelsPerInch>96</o:PixelsPerInch>
+ </o:OfficeDocumentSettings>
+ </xml>
+ </noscript>
+ <![endif]-->
+ <!--[if lte mso 11]>
+ <style type="text/css">
+ .mj-outlook-group-fix {
+ width: 100% !important;
+ }
+ </style>
+ <![endif]-->
+ <!--[if !mso]><!-->
+ <link
+ href="https://fonts.googleapis.com/css?family=Open Sans"
+ rel="stylesheet"
+ type="text/css"
+ />
+ <style type="text/css">
+ @import url(https://fonts.googleapis.com/css?family=Open Sans);
+ </style>
+ <!--<![endif]-->
+ <style type="text/css">
+ @media only screen and (min-width: 480px) {
+ .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ }
+ </style>
+ <style media="screen and (min-width:480px)">
+ .moz-text-html .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ [owa] .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ @media only screen and (max-width: 480px) {
+ table.mj-full-width-mobile {
+ width: 100% !important;
+ }
+
+ td.mj-full-width-mobile {
+ width: auto !important;
+ }
+ }
+ </style>
+ </head>
+
+ <body style="word-spacing: normal; background-color: #f2f7fa">
+ <div style="background-color: #f2f7fa">
+ <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 0px 0px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <br />
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0;
+ padding-bottom: 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-right: 0px;
+ padding-bottom: 40px;
+ padding-left: 0px;
+ word-break: break-word;
+ "
+ >
+ <p
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 100%;
+ "
+ ></p>
+ <!--[if mso | IE
+ ]><table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 600px;
+ "
+ role="presentation"
+ width="600px"
+ >
+ <tr>
+ <td style="height: 0; line-height: 0">
+ &nbsp;
+ </td>
+ </tr>
+ </table><!
+ [endif]-->
+ </td>
+ </tr>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="
+ border-collapse: collapse;
+ border-spacing: 0px;
+ "
+ >
+ <tbody>
+ <tr>
+ <td style="width: 121px">
+ <img
+ alt=""
+ height="auto"
+ src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
+ style="
+ border: none;
+ display: block;
+ outline: none;
+ text-decoration: none;
+ height: auto;
+ width: 100%;
+ font-size: 13px;
+ "
+ width="121"
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 70px;
+ padding-top: 30px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+
+<tbody>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <h1
+ style="
+ text-align: center;
+ color: #000000;
+ line-height: 32px;
+ "
+ >
+ Your Vespa Cloud trial has expired
+ </h1>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+
+<p>
+ Your Vespa Cloud trial has expired. Please reach out to us if you have any questions or feedback.
+</p>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="center"
+ vertical-align="middle"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="border-collapse: separate; line-height: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ bgcolor="#005A8E"
+ role="presentation"
+ style="
+ border: none;
+ border-radius: 100px;
+ cursor: auto;
+ mso-padding-alt: 15px 25px 15px 25px;
+ background: #005a8e;
+ "
+ valign="middle"
+ >
+ <a
+ href="https://dashboard.tld/trial-tenant"
+ style="
+ display: inline-block;
+ background: #005a8e;
+ color: #ffffff;
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 120%;
+ margin: 0;
+ text-decoration: none;
+ text-transform: none;
+ padding: 15px 25px 15px 25px;
+ mso-padding-alt: 0px;
+ border-radius: 100px;
+ "
+ target="_blank"
+ ><b style="font-weight: 700"
+ ><b style="font-weight: 700"
+ >Go to Console</b
+ ></b
+ ></a
+ >
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+</tr>
+</tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 20px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 0px 20px 0px 20px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: center;
+ color: #797e82;
+ "
+ >
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
+ ><span style="color: #005a8e"
+ >Yahoo Privacy Policy</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/terms-of-service-trial.html"
+ ><span style="color: #005a8e"
+ >Terms of Service</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/support"
+ ><span style="color: #005a8e">Support</span></a
+ >
+ </p>
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: inherit; text-decoration: none"
+ href="https://dashboard.tld/tenant/trial-tenant/account/notifications"
+ >Click
+ <span style="color: #005a8e"><u>here</u></span>
+ to manage your notifications setting.</a
+ ><br />
+ </p>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </div>
+ </body>
+</html>
diff --git a/controller-server/src/test/resources/mail/trial-expiring-immediately.html b/controller-server/src/test/resources/mail/trial-expiring-immediately.html
new file mode 100644
index 00000000000..4b16619fe9c
--- /dev/null
+++ b/controller-server/src/test/resources/mail/trial-expiring-immediately.html
@@ -0,0 +1,646 @@
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:v="urn:schemas-microsoft-com:vml"
+ xmlns:o="urn:schemas-microsoft-com:office:office"
+>
+ <head>
+ <title></title>
+ <!--[if !mso]><!-->
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <!--<![endif]-->
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <style type="text/css">
+ #outlook a {
+ padding: 0;
+ }
+
+ body {
+ margin: 0;
+ padding: 0;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ }
+
+ table,
+ td {
+ border-collapse: collapse;
+ mso-table-lspace: 0pt;
+ mso-table-rspace: 0pt;
+ }
+
+ img {
+ border: 0;
+ height: auto;
+ line-height: 100%;
+ outline: none;
+ text-decoration: none;
+ -ms-interpolation-mode: bicubic;
+ }
+
+ p {
+ display: block;
+ margin: 13px 0;
+ }
+ </style>
+ <!--[if mso]>
+ <noscript>
+ <xml>
+ <o:OfficeDocumentSettings>
+ <o:AllowPNG />
+ <o:PixelsPerInch>96</o:PixelsPerInch>
+ </o:OfficeDocumentSettings>
+ </xml>
+ </noscript>
+ <![endif]-->
+ <!--[if lte mso 11]>
+ <style type="text/css">
+ .mj-outlook-group-fix {
+ width: 100% !important;
+ }
+ </style>
+ <![endif]-->
+ <!--[if !mso]><!-->
+ <link
+ href="https://fonts.googleapis.com/css?family=Open Sans"
+ rel="stylesheet"
+ type="text/css"
+ />
+ <style type="text/css">
+ @import url(https://fonts.googleapis.com/css?family=Open Sans);
+ </style>
+ <!--<![endif]-->
+ <style type="text/css">
+ @media only screen and (min-width: 480px) {
+ .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ }
+ </style>
+ <style media="screen and (min-width:480px)">
+ .moz-text-html .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ [owa] .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ @media only screen and (max-width: 480px) {
+ table.mj-full-width-mobile {
+ width: 100% !important;
+ }
+
+ td.mj-full-width-mobile {
+ width: auto !important;
+ }
+ }
+ </style>
+ </head>
+
+ <body style="word-spacing: normal; background-color: #f2f7fa">
+ <div style="background-color: #f2f7fa">
+ <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 0px 0px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <br />
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0;
+ padding-bottom: 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-right: 0px;
+ padding-bottom: 40px;
+ padding-left: 0px;
+ word-break: break-word;
+ "
+ >
+ <p
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 100%;
+ "
+ ></p>
+ <!--[if mso | IE
+ ]><table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 600px;
+ "
+ role="presentation"
+ width="600px"
+ >
+ <tr>
+ <td style="height: 0; line-height: 0">
+ &nbsp;
+ </td>
+ </tr>
+ </table><!
+ [endif]-->
+ </td>
+ </tr>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="
+ border-collapse: collapse;
+ border-spacing: 0px;
+ "
+ >
+ <tbody>
+ <tr>
+ <td style="width: 121px">
+ <img
+ alt=""
+ height="auto"
+ src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
+ style="
+ border: none;
+ display: block;
+ outline: none;
+ text-decoration: none;
+ height: auto;
+ width: 100%;
+ font-size: 13px;
+ "
+ width="121"
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 70px;
+ padding-top: 30px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+
+<tbody>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <h1
+ style="
+ text-align: center;
+ color: #000000;
+ line-height: 32px;
+ "
+ >
+ Your Vespa Cloud trial expires tomorrow
+ </h1>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+
+<p>
+ Your Vespa Cloud trial expires tomorrow. Please reach out to us if you have any questions or feedback.
+</p>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="center"
+ vertical-align="middle"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="border-collapse: separate; line-height: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ bgcolor="#005A8E"
+ role="presentation"
+ style="
+ border: none;
+ border-radius: 100px;
+ cursor: auto;
+ mso-padding-alt: 15px 25px 15px 25px;
+ background: #005a8e;
+ "
+ valign="middle"
+ >
+ <a
+ href="https://dashboard.tld/trial-tenant"
+ style="
+ display: inline-block;
+ background: #005a8e;
+ color: #ffffff;
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 120%;
+ margin: 0;
+ text-decoration: none;
+ text-transform: none;
+ padding: 15px 25px 15px 25px;
+ mso-padding-alt: 0px;
+ border-radius: 100px;
+ "
+ target="_blank"
+ ><b style="font-weight: 700"
+ ><b style="font-weight: 700"
+ >Go to Console</b
+ ></b
+ ></a
+ >
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+</tr>
+</tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 20px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 0px 20px 0px 20px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: center;
+ color: #797e82;
+ "
+ >
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
+ ><span style="color: #005a8e"
+ >Yahoo Privacy Policy</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/terms-of-service-trial.html"
+ ><span style="color: #005a8e"
+ >Terms of Service</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/support"
+ ><span style="color: #005a8e">Support</span></a
+ >
+ </p>
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: inherit; text-decoration: none"
+ href="https://dashboard.tld/tenant/trial-tenant/account/notifications"
+ >Click
+ <span style="color: #005a8e"><u>here</u></span>
+ to manage your notifications setting.</a
+ ><br />
+ </p>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </div>
+ </body>
+</html>
diff --git a/controller-server/src/test/resources/mail/trial-expiring-soon.html b/controller-server/src/test/resources/mail/trial-expiring-soon.html
new file mode 100644
index 00000000000..b4c85173171
--- /dev/null
+++ b/controller-server/src/test/resources/mail/trial-expiring-soon.html
@@ -0,0 +1,646 @@
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:v="urn:schemas-microsoft-com:vml"
+ xmlns:o="urn:schemas-microsoft-com:office:office"
+>
+ <head>
+ <title></title>
+ <!--[if !mso]><!-->
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <!--<![endif]-->
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <style type="text/css">
+ #outlook a {
+ padding: 0;
+ }
+
+ body {
+ margin: 0;
+ padding: 0;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ }
+
+ table,
+ td {
+ border-collapse: collapse;
+ mso-table-lspace: 0pt;
+ mso-table-rspace: 0pt;
+ }
+
+ img {
+ border: 0;
+ height: auto;
+ line-height: 100%;
+ outline: none;
+ text-decoration: none;
+ -ms-interpolation-mode: bicubic;
+ }
+
+ p {
+ display: block;
+ margin: 13px 0;
+ }
+ </style>
+ <!--[if mso]>
+ <noscript>
+ <xml>
+ <o:OfficeDocumentSettings>
+ <o:AllowPNG />
+ <o:PixelsPerInch>96</o:PixelsPerInch>
+ </o:OfficeDocumentSettings>
+ </xml>
+ </noscript>
+ <![endif]-->
+ <!--[if lte mso 11]>
+ <style type="text/css">
+ .mj-outlook-group-fix {
+ width: 100% !important;
+ }
+ </style>
+ <![endif]-->
+ <!--[if !mso]><!-->
+ <link
+ href="https://fonts.googleapis.com/css?family=Open Sans"
+ rel="stylesheet"
+ type="text/css"
+ />
+ <style type="text/css">
+ @import url(https://fonts.googleapis.com/css?family=Open Sans);
+ </style>
+ <!--<![endif]-->
+ <style type="text/css">
+ @media only screen and (min-width: 480px) {
+ .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ }
+ </style>
+ <style media="screen and (min-width:480px)">
+ .moz-text-html .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ [owa] .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ @media only screen and (max-width: 480px) {
+ table.mj-full-width-mobile {
+ width: 100% !important;
+ }
+
+ td.mj-full-width-mobile {
+ width: auto !important;
+ }
+ }
+ </style>
+ </head>
+
+ <body style="word-spacing: normal; background-color: #f2f7fa">
+ <div style="background-color: #f2f7fa">
+ <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 0px 0px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <br />
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0;
+ padding-bottom: 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-right: 0px;
+ padding-bottom: 40px;
+ padding-left: 0px;
+ word-break: break-word;
+ "
+ >
+ <p
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 100%;
+ "
+ ></p>
+ <!--[if mso | IE
+ ]><table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 600px;
+ "
+ role="presentation"
+ width="600px"
+ >
+ <tr>
+ <td style="height: 0; line-height: 0">
+ &nbsp;
+ </td>
+ </tr>
+ </table><!
+ [endif]-->
+ </td>
+ </tr>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="
+ border-collapse: collapse;
+ border-spacing: 0px;
+ "
+ >
+ <tbody>
+ <tr>
+ <td style="width: 121px">
+ <img
+ alt=""
+ height="auto"
+ src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
+ style="
+ border: none;
+ display: block;
+ outline: none;
+ text-decoration: none;
+ height: auto;
+ width: 100%;
+ font-size: 13px;
+ "
+ width="121"
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 70px;
+ padding-top: 30px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+
+<tbody>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <h1
+ style="
+ text-align: center;
+ color: #000000;
+ line-height: 32px;
+ "
+ >
+ Your Vespa Cloud trial expires in 2 days
+ </h1>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+
+<p>
+ Your Vespa Cloud trial expires in 2 days. Please reach out to us if you have any questions or feedback.
+</p>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="center"
+ vertical-align="middle"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="border-collapse: separate; line-height: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ bgcolor="#005A8E"
+ role="presentation"
+ style="
+ border: none;
+ border-radius: 100px;
+ cursor: auto;
+ mso-padding-alt: 15px 25px 15px 25px;
+ background: #005a8e;
+ "
+ valign="middle"
+ >
+ <a
+ href="https://dashboard.tld/trial-tenant"
+ style="
+ display: inline-block;
+ background: #005a8e;
+ color: #ffffff;
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 120%;
+ margin: 0;
+ text-decoration: none;
+ text-transform: none;
+ padding: 15px 25px 15px 25px;
+ mso-padding-alt: 0px;
+ border-radius: 100px;
+ "
+ target="_blank"
+ ><b style="font-weight: 700"
+ ><b style="font-weight: 700"
+ >Go to Console</b
+ ></b
+ ></a
+ >
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+</tr>
+</tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 20px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 0px 20px 0px 20px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: center;
+ color: #797e82;
+ "
+ >
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
+ ><span style="color: #005a8e"
+ >Yahoo Privacy Policy</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/terms-of-service-trial.html"
+ ><span style="color: #005a8e"
+ >Terms of Service</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/support"
+ ><span style="color: #005a8e">Support</span></a
+ >
+ </p>
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: inherit; text-decoration: none"
+ href="https://dashboard.tld/tenant/trial-tenant/account/notifications"
+ >Click
+ <span style="color: #005a8e"><u>here</u></span>
+ to manage your notifications setting.</a
+ ><br />
+ </p>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </div>
+ </body>
+</html>
diff --git a/controller-server/src/test/resources/mail/trial-reminder.html b/controller-server/src/test/resources/mail/trial-reminder.html
new file mode 100644
index 00000000000..2644b187764
--- /dev/null
+++ b/controller-server/src/test/resources/mail/trial-reminder.html
@@ -0,0 +1,646 @@
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:v="urn:schemas-microsoft-com:vml"
+ xmlns:o="urn:schemas-microsoft-com:office:office"
+>
+ <head>
+ <title></title>
+ <!--[if !mso]><!-->
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <!--<![endif]-->
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <style type="text/css">
+ #outlook a {
+ padding: 0;
+ }
+
+ body {
+ margin: 0;
+ padding: 0;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ }
+
+ table,
+ td {
+ border-collapse: collapse;
+ mso-table-lspace: 0pt;
+ mso-table-rspace: 0pt;
+ }
+
+ img {
+ border: 0;
+ height: auto;
+ line-height: 100%;
+ outline: none;
+ text-decoration: none;
+ -ms-interpolation-mode: bicubic;
+ }
+
+ p {
+ display: block;
+ margin: 13px 0;
+ }
+ </style>
+ <!--[if mso]>
+ <noscript>
+ <xml>
+ <o:OfficeDocumentSettings>
+ <o:AllowPNG />
+ <o:PixelsPerInch>96</o:PixelsPerInch>
+ </o:OfficeDocumentSettings>
+ </xml>
+ </noscript>
+ <![endif]-->
+ <!--[if lte mso 11]>
+ <style type="text/css">
+ .mj-outlook-group-fix {
+ width: 100% !important;
+ }
+ </style>
+ <![endif]-->
+ <!--[if !mso]><!-->
+ <link
+ href="https://fonts.googleapis.com/css?family=Open Sans"
+ rel="stylesheet"
+ type="text/css"
+ />
+ <style type="text/css">
+ @import url(https://fonts.googleapis.com/css?family=Open Sans);
+ </style>
+ <!--<![endif]-->
+ <style type="text/css">
+ @media only screen and (min-width: 480px) {
+ .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ }
+ </style>
+ <style media="screen and (min-width:480px)">
+ .moz-text-html .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ [owa] .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ @media only screen and (max-width: 480px) {
+ table.mj-full-width-mobile {
+ width: 100% !important;
+ }
+
+ td.mj-full-width-mobile {
+ width: auto !important;
+ }
+ }
+ </style>
+ </head>
+
+ <body style="word-spacing: normal; background-color: #f2f7fa">
+ <div style="background-color: #f2f7fa">
+ <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 0px 0px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <br />
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0;
+ padding-bottom: 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-right: 0px;
+ padding-bottom: 40px;
+ padding-left: 0px;
+ word-break: break-word;
+ "
+ >
+ <p
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 100%;
+ "
+ ></p>
+ <!--[if mso | IE
+ ]><table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 600px;
+ "
+ role="presentation"
+ width="600px"
+ >
+ <tr>
+ <td style="height: 0; line-height: 0">
+ &nbsp;
+ </td>
+ </tr>
+ </table><!
+ [endif]-->
+ </td>
+ </tr>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="
+ border-collapse: collapse;
+ border-spacing: 0px;
+ "
+ >
+ <tbody>
+ <tr>
+ <td style="width: 121px">
+ <img
+ alt=""
+ height="auto"
+ src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
+ style="
+ border: none;
+ display: block;
+ outline: none;
+ text-decoration: none;
+ height: auto;
+ width: 100%;
+ font-size: 13px;
+ "
+ width="121"
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 70px;
+ padding-top: 30px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+
+<tbody>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <h1
+ style="
+ text-align: center;
+ color: #000000;
+ line-height: 32px;
+ "
+ >
+ How is your Vespa Cloud trial going?
+ </h1>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+
+<p>
+ How is your Vespa Cloud trial going? Please reach out to us if you have any questions or feedback.
+</p>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="center"
+ vertical-align="middle"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="border-collapse: separate; line-height: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ bgcolor="#005A8E"
+ role="presentation"
+ style="
+ border: none;
+ border-radius: 100px;
+ cursor: auto;
+ mso-padding-alt: 15px 25px 15px 25px;
+ background: #005a8e;
+ "
+ valign="middle"
+ >
+ <a
+ href="https://dashboard.tld/trial-tenant"
+ style="
+ display: inline-block;
+ background: #005a8e;
+ color: #ffffff;
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 120%;
+ margin: 0;
+ text-decoration: none;
+ text-transform: none;
+ padding: 15px 25px 15px 25px;
+ mso-padding-alt: 0px;
+ border-radius: 100px;
+ "
+ target="_blank"
+ ><b style="font-weight: 700"
+ ><b style="font-weight: 700"
+ >Go to Console</b
+ ></b
+ ></a
+ >
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+</tr>
+</tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 20px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 0px 20px 0px 20px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: center;
+ color: #797e82;
+ "
+ >
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
+ ><span style="color: #005a8e"
+ >Yahoo Privacy Policy</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/terms-of-service-trial.html"
+ ><span style="color: #005a8e"
+ >Terms of Service</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/support"
+ ><span style="color: #005a8e">Support</span></a
+ >
+ </p>
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: inherit; text-decoration: none"
+ href="https://dashboard.tld/tenant/trial-tenant/account/notifications"
+ >Click
+ <span style="color: #005a8e"><u>here</u></span>
+ to manage your notifications setting.</a
+ ><br />
+ </p>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </div>
+ </body>
+</html>
diff --git a/controller-server/src/test/resources/mail/welcome.html b/controller-server/src/test/resources/mail/welcome.html
new file mode 100644
index 00000000000..a21a7cdf45f
--- /dev/null
+++ b/controller-server/src/test/resources/mail/welcome.html
@@ -0,0 +1,646 @@
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:v="urn:schemas-microsoft-com:vml"
+ xmlns:o="urn:schemas-microsoft-com:office:office"
+>
+ <head>
+ <title></title>
+ <!--[if !mso]><!-->
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <!--<![endif]-->
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <style type="text/css">
+ #outlook a {
+ padding: 0;
+ }
+
+ body {
+ margin: 0;
+ padding: 0;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ }
+
+ table,
+ td {
+ border-collapse: collapse;
+ mso-table-lspace: 0pt;
+ mso-table-rspace: 0pt;
+ }
+
+ img {
+ border: 0;
+ height: auto;
+ line-height: 100%;
+ outline: none;
+ text-decoration: none;
+ -ms-interpolation-mode: bicubic;
+ }
+
+ p {
+ display: block;
+ margin: 13px 0;
+ }
+ </style>
+ <!--[if mso]>
+ <noscript>
+ <xml>
+ <o:OfficeDocumentSettings>
+ <o:AllowPNG />
+ <o:PixelsPerInch>96</o:PixelsPerInch>
+ </o:OfficeDocumentSettings>
+ </xml>
+ </noscript>
+ <![endif]-->
+ <!--[if lte mso 11]>
+ <style type="text/css">
+ .mj-outlook-group-fix {
+ width: 100% !important;
+ }
+ </style>
+ <![endif]-->
+ <!--[if !mso]><!-->
+ <link
+ href="https://fonts.googleapis.com/css?family=Open Sans"
+ rel="stylesheet"
+ type="text/css"
+ />
+ <style type="text/css">
+ @import url(https://fonts.googleapis.com/css?family=Open Sans);
+ </style>
+ <!--<![endif]-->
+ <style type="text/css">
+ @media only screen and (min-width: 480px) {
+ .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ }
+ </style>
+ <style media="screen and (min-width:480px)">
+ .moz-text-html .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ [owa] .mj-column-per-100 {
+ width: 100% !important;
+ max-width: 100%;
+ }
+ </style>
+ <style type="text/css">
+ @media only screen and (max-width: 480px) {
+ table.mj-full-width-mobile {
+ width: 100% !important;
+ }
+
+ td.mj-full-width-mobile {
+ width: auto !important;
+ }
+ }
+ </style>
+ </head>
+
+ <body style="word-spacing: normal; background-color: #f2f7fa">
+ <div style="background-color: #f2f7fa">
+ <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 0px 0px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <br />
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0;
+ padding-bottom: 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ padding-top: 0px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-right: 0px;
+ padding-bottom: 40px;
+ padding-left: 0px;
+ word-break: break-word;
+ "
+ >
+ <p
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 100%;
+ "
+ ></p>
+ <!--[if mso | IE
+ ]><table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ style="
+ border-top: solid 8px #005a8e;
+ font-size: 1px;
+ margin: 0px auto;
+ width: 600px;
+ "
+ role="presentation"
+ width="600px"
+ >
+ <tr>
+ <td style="height: 0; line-height: 0">
+ &nbsp;
+ </td>
+ </tr>
+ </table><!
+ [endif]-->
+ </td>
+ </tr>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="
+ border-collapse: collapse;
+ border-spacing: 0px;
+ "
+ >
+ <tbody>
+ <tr>
+ <td style="width: 121px">
+ <img
+ alt=""
+ height="auto"
+ src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
+ style="
+ border: none;
+ display: block;
+ outline: none;
+ text-decoration: none;
+ height: auto;
+ width: 100%;
+ font-size: 13px;
+ "
+ width="121"
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div
+ style="
+ background: #ffffff;
+ background-color: #ffffff;
+ margin: 0px auto;
+ max-width: 600px;
+ "
+ >
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="background: #ffffff; background-color: #ffffff; width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 70px;
+ padding-top: 30px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+
+<tbody>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+ <h1
+ style="
+ text-align: center;
+ color: #000000;
+ line-height: 32px;
+ "
+ >
+ Welcome to Vespa Cloud
+ </h1>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="left"
+ style="
+ font-size: 0px;
+ padding: 0px 25px 0px 25px;
+ padding-top: 0px;
+ padding-right: 50px;
+ padding-bottom: 0px;
+ padding-left: 50px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ line-height: 22px;
+ text-align: left;
+ color: #797e82;
+ "
+ >
+
+<p>
+ Welcome to Vespa Cloud! We hope you will enjoy your trial. Please reach out to us if you have any questions or feedback.
+</p>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td
+ align="center"
+ vertical-align="middle"
+ style="
+ font-size: 0px;
+ padding: 10px 25px;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ word-break: break-word;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="border-collapse: separate; line-height: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ bgcolor="#005A8E"
+ role="presentation"
+ style="
+ border: none;
+ border-radius: 100px;
+ cursor: auto;
+ mso-padding-alt: 15px 25px 15px 25px;
+ background: #005a8e;
+ "
+ valign="middle"
+ >
+ <a
+ href="https://dashboard.tld/trial-tenant"
+ style="
+ display: inline-block;
+ background: #005a8e;
+ color: #ffffff;
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 120%;
+ margin: 0;
+ text-decoration: none;
+ text-transform: none;
+ padding: 15px 25px 15px 25px;
+ mso-padding-alt: 0px;
+ border-radius: 100px;
+ "
+ target="_blank"
+ ><b style="font-weight: 700"
+ ><b style="font-weight: 700"
+ >Go to Console</b
+ ></b
+ ></a
+ >
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+</tr>
+</tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+ <div style="margin: 0px auto; max-width: 600px">
+ <table
+ align="center"
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="width: 100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ style="
+ direction: ltr;
+ font-size: 0px;
+ padding: 20px 0px 20px 0px;
+ padding-bottom: 0px;
+ padding-top: 20px;
+ text-align: center;
+ "
+ >
+ <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+ <div
+ class="mj-column-per-100 mj-outlook-group-fix"
+ style="
+ font-size: 0px;
+ text-align: left;
+ direction: ltr;
+ display: inline-block;
+ vertical-align: top;
+ width: 100%;
+ "
+ >
+ <table
+ border="0"
+ cellpadding="0"
+ cellspacing="0"
+ role="presentation"
+ style="vertical-align: top"
+ width="100%"
+ >
+ <tbody>
+ <tr>
+ <td
+ align="center"
+ style="
+ font-size: 0px;
+ padding: 0px 20px 0px 20px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ word-break: break-word;
+ "
+ >
+ <div
+ style="
+ font-family: Open Sans, Helvetica, Arial,
+ sans-serif;
+ font-size: 11px;
+ line-height: 22px;
+ text-align: center;
+ color: #797e82;
+ "
+ >
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
+ ><span style="color: #005a8e"
+ >Yahoo Privacy Policy</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/terms-of-service-trial.html"
+ ><span style="color: #005a8e"
+ >Terms of Service</span
+ ></a
+ ><span style="color: #797e82"
+ >&nbsp; &nbsp;|&nbsp; &nbsp;</span
+ ><a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: #005a8e"
+ href="https://dashboard.tld/support"
+ ><span style="color: #005a8e">Support</span></a
+ >
+ </p>
+ <p style="margin: 10px 0">
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ style="color: inherit; text-decoration: none"
+ href="https://dashboard.tld/tenant/trial-tenant/account/notifications"
+ >Click
+ <span style="color: #005a8e"><u>here</u></span>
+ to manage your notifications setting.</a
+ ><br />
+ </p>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!--[if mso | IE]></td></tr></table><![endif]-->
+ </div>
+ </body>
+</html>
diff --git a/documentapi/src/tests/policies/policies_test.cpp b/documentapi/src/tests/policies/policies_test.cpp
index 9dd73a71920..7091b63b6b3 100644
--- a/documentapi/src/tests/policies/policies_test.cpp
+++ b/documentapi/src/tests/policies/policies_test.cpp
@@ -806,7 +806,7 @@ Test::requireThatContentPolicyIsRandomWithoutState()
ContentPolicy &policy = setupContentPolicy(
frame, param,
"storage/cluster.mycluster/distributor/*/default", 5);
- ASSERT_TRUE(policy.getSystemState() == nullptr);
+ ASSERT_FALSE(policy.getSystemState());
std::set<string> lst;
for (uint32_t i = 0; i < 666; i++) {
@@ -858,12 +858,12 @@ Test::requireThatContentPolicyIsTargetedWithState()
"cluster=mycluster;slobroks=tcp/localhost:%d;clusterconfigid=%s;syncinit",
slobrok.port(), getDefaultDistributionConfig(2, 5).c_str());
ContentPolicy &policy = setupContentPolicy(frame, param, "storage/cluster.mycluster/distributor/*/default", 5);
- ASSERT_TRUE(policy.getSystemState() == nullptr);
+ ASSERT_FALSE(policy.getSystemState());
{
std::vector<mbus::RoutingNode*> leaf;
ASSERT_TRUE(frame.select(leaf, 1));
leaf[0]->handleReply(std::make_unique<WrongDistributionReply>("distributor:5 storage:5"));
- ASSERT_TRUE(policy.getSystemState() != nullptr);
+ ASSERT_TRUE(policy.getSystemState());
EXPECT_EQUAL(policy.getSystemState()->toString(), "distributor:5 storage:5");
}
std::set<string> lst;
@@ -897,12 +897,12 @@ Test::requireThatContentPolicyCombinesSystemAndSlobrokState()
ContentPolicy &policy = setupContentPolicy(
frame, param,
"storage/cluster.mycluster/distributor/*/default", 1);
- ASSERT_TRUE(policy.getSystemState() == nullptr);
+ ASSERT_FALSE(policy.getSystemState());
{
std::vector<mbus::RoutingNode*> leaf;
ASSERT_TRUE(frame.select(leaf, 1));
leaf[0]->handleReply(std::make_unique<WrongDistributionReply>("distributor:99 storage:99"));
- ASSERT_TRUE(policy.getSystemState() != nullptr);
+ ASSERT_TRUE(policy.getSystemState());
EXPECT_EQUAL(policy.getSystemState()->toString(), "distributor:99 storage:99");
}
for (int i = 0; i < 666; i++) {
diff --git a/documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.cpp b/documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.cpp
index e391699b750..ea27d42e790 100644
--- a/documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.cpp
+++ b/documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.cpp
@@ -43,7 +43,7 @@ namespace {
class CallBack : public config::IFetcherCallback<storage::lib::Distribution::DistributionConfig>
{
public:
- CallBack(ContentPolicy & policy) : _policy(policy) { }
+ explicit CallBack(ContentPolicy & policy) : _policy(policy) { }
void configure(std::unique_ptr<storage::lib::Distribution::DistributionConfig> config) override {
_policy.configure(std::move(config));
}
@@ -78,13 +78,13 @@ string ContentPolicy::init()
ContentPolicy::~ContentPolicy() = default;
string
-ContentPolicy::createConfigId(const string & clusterName) const
+ContentPolicy::createConfigId(const string & clusterName)
{
return clusterName;
}
string
-ContentPolicy::createPattern(const string & clusterName, int distributor) const
+ContentPolicy::createPattern(const string & clusterName, int distributor)
{
vespalib::asciistream ost;
@@ -103,7 +103,8 @@ void
ContentPolicy::configure(std::unique_ptr<vespa::config::content::StorDistributionConfig> config)
{
try {
- _nextDistribution = std::make_unique<storage::lib::Distribution>(*config);
+ std::lock_guard guard(_rw_lock);
+ _distribution = std::make_unique<storage::lib::Distribution>(*config);
} catch (const std::exception& e) {
LOG(warning, "Got exception when configuring distribution, config id was %s", _clusterConfigId.c_str());
throw e;
@@ -116,8 +117,9 @@ ContentPolicy::doSelect(mbus::RoutingContext &context)
const mbus::Message &msg = context.getMessage();
int distributor = -1;
+ auto [cur_state, cur_distribution] = internal_state_snapshot();
- if (_state.get()) {
+ if (cur_state) {
document::BucketId id;
switch(msg.getType()) {
case DocumentProtocol::MESSAGE_PUTDOCUMENT:
@@ -168,15 +170,10 @@ ContentPolicy::doSelect(mbus::RoutingContext &context)
// Pick a distributor using ideal state algorithm
try {
- // Update distribution here, to make it not take lock in average case
- if (_nextDistribution) {
- _distribution = std::move(_nextDistribution);
- _nextDistribution.reset();
- }
- assert(_distribution.get());
- distributor = _distribution->getIdealDistributorNode(*_state, id);
+ assert(cur_distribution);
+ distributor = cur_distribution->getIdealDistributorNode(*cur_state, id);
} catch (storage::lib::TooFewBucketBitsInUseException& e) {
- auto reply = std::make_unique<WrongDistributionReply>(_state->toString());
+ auto reply = std::make_unique<WrongDistributionReply>(cur_state->toString());
reply->addError(mbus::Error(
DocumentProtocol::ERROR_WRONG_DISTRIBUTION,
"Too few distribution bits used for given cluster state"));
@@ -185,7 +182,7 @@ ContentPolicy::doSelect(mbus::RoutingContext &context)
} catch (storage::lib::NoDistributorsAvailableException& e) {
// No distributors available in current cluster state. Remove
// cluster state we cannot use and send to random target
- _state.reset();
+ reset_state();
distributor = -1;
}
}
@@ -216,7 +213,7 @@ ContentPolicy::getRecipient(mbus::RoutingContext& context, int distributor)
return mbus::Hop::parse(entries[random() % entries.size()].second + "/default");
}
- return mbus::Hop();
+ return {};
}
void
@@ -226,9 +223,9 @@ ContentPolicy::merge(mbus::RoutingContext &context)
mbus::Reply::UP reply = it.removeReply();
if (reply->getType() == DocumentProtocol::REPLY_WRONGDISTRIBUTION) {
- updateStateFromReply(static_cast<WrongDistributionReply&>(*reply));
+ updateStateFromReply(dynamic_cast<WrongDistributionReply&>(*reply));
} else if (reply->hasErrors()) {
- _state.reset();
+ reset_state();
}
context.setReply(std::move(reply));
@@ -237,8 +234,8 @@ ContentPolicy::merge(mbus::RoutingContext &context)
void
ContentPolicy::updateStateFromReply(WrongDistributionReply& wdr)
{
- std::unique_ptr<storage::lib::ClusterState> newState(
- new storage::lib::ClusterState(wdr.getSystemState()));
+ auto newState = std::make_unique<storage::lib::ClusterState>(wdr.getSystemState());
+ std::lock_guard guard(_rw_lock);
if (!_state || newState->getVersion() >= _state->getVersion()) {
if (_state) {
wdr.getTrace().trace(1, make_string("System state changed from version %u to %u",
@@ -256,4 +253,28 @@ ContentPolicy::updateStateFromReply(WrongDistributionReply& wdr)
}
}
+ContentPolicy::StateSnapshot
+ContentPolicy::internal_state_snapshot()
+{
+ std::shared_lock guard(_rw_lock);
+ return {_state, _distribution};
+}
+
+std::shared_ptr<const storage::lib::ClusterState>
+ContentPolicy::getSystemState() const noexcept
+{
+ std::shared_lock guard(_rw_lock);
+ return _state;
+}
+
+void
+ContentPolicy::reset_state()
+{
+ // It's possible for the caller to race between checking and resetting the state,
+ // but this should never lead to a worse outcome than sending to a random distributor
+ // as if no state had been cached prior.
+ std::lock_guard guard(_rw_lock);
+ _state.reset();
+}
+
} // documentapi
diff --git a/documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.h b/documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.h
index e49ad378b90..7a3675c3001 100644
--- a/documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.h
+++ b/documentapi/src/vespa/documentapi/messagebus/policies/contentpolicy.h
@@ -6,55 +6,62 @@
#include <vespa/vdslib/distribution/distribution.h>
#include <vespa/document/bucket/bucketidfactory.h>
#include <vespa/messagebus/routing/hop.h>
+#include <shared_mutex>
namespace config {
class ICallback;
class ConfigFetcher;
}
-namespace storage {
-namespace lib {
+namespace storage::lib {
class Distribution;
class ClusterState;
}
-}
namespace documentapi {
class ContentPolicy : public ExternSlobrokPolicy
{
private:
- document::BucketIdFactory _bucketIdFactory;
- std::unique_ptr<storage::lib::ClusterState> _state;
- string _clusterName;
- string _clusterConfigId;
- std::unique_ptr<config::ICallback> _callBack;
- std::unique_ptr<config::ConfigFetcher> _configFetcher;
- std::unique_ptr<storage::lib::Distribution> _distribution;
- std::unique_ptr<storage::lib::Distribution> _nextDistribution;
+ document::BucketIdFactory _bucketIdFactory;
+ mutable std::shared_mutex _rw_lock;
+ std::shared_ptr<const storage::lib::ClusterState> _state;
+ string _clusterName;
+ string _clusterConfigId;
+ std::unique_ptr<config::ICallback> _callBack;
+ std::unique_ptr<config::ConfigFetcher> _configFetcher;
+ std::shared_ptr<const storage::lib::Distribution> _distribution;
+
+ using StateSnapshot = std::pair<std::shared_ptr<const storage::lib::ClusterState>,
+ std::shared_ptr<const storage::lib::Distribution>>;
+
+ // Acquires _lock
+ [[nodiscard]] StateSnapshot internal_state_snapshot();
mbus::Hop getRecipient(mbus::RoutingContext& context, int distributor);
+ // Acquires _lock
+ void updateStateFromReply(WrongDistributionReply& reply);
+ // Acquires _lock
+ void reset_state();
public:
- ContentPolicy(const string& param);
- ~ContentPolicy();
+ explicit ContentPolicy(const string& param);
+ ~ContentPolicy() override;
void doSelect(mbus::RoutingContext &context) override;
void merge(mbus::RoutingContext &context) override;
- void updateStateFromReply(WrongDistributionReply& reply);
-
/**
* @return a pointer to the system state registered with this policy. If
- * we haven't received a system state yet, returns NULL.
+ * we haven't received a system state yet, returns nullptr.
*/
- const storage::lib::ClusterState* getSystemState() const { return _state.get(); }
+ std::shared_ptr<const storage::lib::ClusterState> getSystemState() const noexcept;
virtual void configure(std::unique_ptr<storage::lib::Distribution::DistributionConfig> config);
string init() override;
private:
- string createConfigId(const string & clusterName) const;
- string createPattern(const string & clusterName, int distributor) const;
+ static string createConfigId(const string & clusterName);
+ static string createPattern(const string & clusterName, int distributor);
};
}
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 2b3fe84ec84..d02cccb2885 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
@@ -309,12 +309,6 @@ public class Flags {
"Whether to enable CrowdStrike.", "Takes effect on next host admin tick",
HOSTNAME);
- public static final UnboundBooleanFlag RANDOMIZED_ENDPOINT_NAMES = defineFeatureFlag(
- "randomized-endpoint-names", false, List.of("andreer"), "2023-04-26", "2023-11-14",
- "Whether to use randomized endpoint names",
- "Takes effect on application deployment",
- INSTANCE_ID, APPLICATION_ID, TENANT_ID);
-
public static final UnboundBooleanFlag ENABLE_THE_ONE_THAT_SHOULD_NOT_BE_NAMED = defineFeatureFlag(
"enable-the-one-that-should-not-be-named", false, List.of("hmusum"), "2023-05-08", "2023-11-01",
"Whether to enable the one program that should not be named",
@@ -340,6 +334,13 @@ public class Flags {
"Takes effect at redeployment",
INSTANCE_ID);
+ public static final UnboundBooleanFlag EXCLUSIVE_PROVISIONING = defineFeatureFlag(
+ "exclusive-provisioning", false,
+ List.of("hakonhall"), "2023-10-12", "2023-12-12",
+ "Whether to provision a host exclusively to an application ID only based on exclusive=\"true\" from services.xml. " +
+ "Enabling this will produce hosts with exclusiveTo[ApplicationId] without provisionedToApplicationId.",
+ "Takes immediate effect when provisioning new hosts");
+
public static final UnboundBooleanFlag WRITE_CONFIG_SERVER_SESSION_DATA_AS_ONE_BLOB = defineFeatureFlag(
"write-config-server-session-data-as-blob", false,
List.of("hmusum"), "2023-07-19", "2023-11-01",
@@ -380,20 +381,6 @@ public class Flags {
"Takes effect immediately",
INSTANCE_ID, CLUSTER_ID, CLUSTER_TYPE);
- public static final UnboundBooleanFlag ASSIGN_RANDOMIZED_ID = defineFeatureFlag(
- "assign-randomized-id", true,
- List.of("mortent"), "2023-08-31", "2024-02-01",
- "Whether to assign randomized id to the application",
- "Takes effect immediately",
- INSTANCE_ID);
-
- public static final UnboundIntFlag ASSIGNED_RANDOMIZED_ID_RATE = defineIntFlag(
- "assign-randomized-id-rate", 5,
- List.of("mortent"), "2023-09-11", "2024-02-01",
- "Rate for requesting assigned ids for existing certificates. Rate is per maintainer cycle.",
- "Takes effect immediately",
- INSTANCE_ID);
-
public static final UnboundIntFlag CONTENT_LAYER_METADATA_FEATURE_LEVEL = defineIntFlag(
"content-layer-metadata-feature-level", 0,
List.of("vekterli"), "2022-09-12", "2024-02-01",
@@ -416,12 +403,6 @@ public class Flags {
"Takes effect at redeployment",
INSTANCE_ID);
- public static final UnboundBooleanFlag LEGACY_ENDPOINTS = defineFeatureFlag(
- "legacy-endpoints", true, List.of("mpolden", "tokle"), "2023-09-29", "2024-03-01",
- "Whether legacy (non-anonymized) endpoints should be created in DNS",
- "Takes effect on redeployment through controller",
- INSTANCE_ID, APPLICATION_ID, TENANT_ID);
-
public static final UnboundIntFlag SEARCH_HANDLER_THREADPOOL = defineIntFlag(
"search-handler-threadpool", 2,
List.of("bjorncs", "baldersheim"), "2023-10-01", "2024-01-01",
diff --git a/metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java b/metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java
index f03c54aa822..34cf2d98ef8 100644
--- a/metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java
+++ b/metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java
@@ -61,7 +61,12 @@ public enum ControllerMetrics implements VespaMetrics {
METERING_MEMORY_GB("metering.memoryGB", Unit.GIGABYTE, "Controller: Metering memory GB"),
METERING_VCPU("metering.vcpu", Unit.VCPU, "Controller: Metering VCPU"),
METERING_LAST_REPORTED("metering_last_reported", Unit.SECONDS_SINCE_EPOCH, "Controller: Metering last reported"),
- METERING_TOTAL_REPORTED("metering_total_reported", Unit.ITEM, "Controller: Metering total reported (sum of resources)");
+ METERING_TOTAL_REPORTED("metering_total_reported", Unit.ITEM, "Controller: Metering total reported (sum of resources)"),
+
+ MAIL_SENT("mail.sent", Unit.OPERATION, "Mail sent"),
+ MAIL_FAILED("mail.failed", Unit.OPERATION, "Mail delivery failed"),
+ MAIL_THROTTLED("mail.throttled", Unit.OPERATION, "Mail delivery throttled");
+
private final String name;
private final Unit unit;
diff --git a/metrics/src/main/java/ai/vespa/metrics/docs/MetricSetDocumentation.java b/metrics/src/main/java/ai/vespa/metrics/docs/MetricSetDocumentation.java
index b046d55c089..a15f2916091 100644
--- a/metrics/src/main/java/ai/vespa/metrics/docs/MetricSetDocumentation.java
+++ b/metrics/src/main/java/ai/vespa/metrics/docs/MetricSetDocumentation.java
@@ -45,8 +45,14 @@ public class MetricSetDocumentation {
referenceBuilder.append(String.format("""
---
# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+ # Note: This file is generated by
+ # https://github.com/vespa-engine/vespa/blob/master/metrics/src/main/java/ai/vespa/metrics/docs/MetricSetDocumentation.java
title: "%s Metric Set"
- ---""", name));
+ ---
+ <p>
+ This document provides reference documentation for the %s metric set, including suffixes present per metric.
+ If the suffix column contains "N/A" then the base name of the corresponding metric is used with no suffix.
+ </p>""", name, name));
metricsByType.keySet()
.stream()
.sorted()
diff --git a/metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java b/metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java
index 9443a08e28b..36750adb749 100644
--- a/metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java
+++ b/metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java
@@ -181,6 +181,10 @@ public class InfrastructureMetricSet {
addMetric(metrics, ControllerMetrics.METERING_AGE_SECONDS.min());
addMetric(metrics, ControllerMetrics.METERING_LAST_REPORTED.max());
+ addMetric(metrics, ControllerMetrics.MAIL_SENT.count());
+ addMetric(metrics, ControllerMetrics.MAIL_FAILED.count());
+ addMetric(metrics, ControllerMetrics.MAIL_THROTTLED.count());
+
return metrics;
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java
index 09d6f96d88e..a876999e80b 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java
@@ -22,14 +22,21 @@ public interface HostProvisioner {
enum HostSharing {
- /** The host must be provisioned exclusively for the applicationId */
+ /** The host must be provisioned exclusively for the application ID. */
+ provision,
+
+ /** The host must be exclusive to a single application ID */
exclusive,
/** The host must be provisioned to be shared with other applications. */
shared,
/** The client has no requirements on whether the host must be provisioned exclusively or shared. */
- any
+ any;
+
+ public boolean isExclusiveAllocation() {
+ return this == provision || this == exclusive;
+ }
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java
index 0ffd42aedba..89ff0938d59 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java
@@ -208,7 +208,9 @@ public class Preparer {
private HostSharing hostSharing(ClusterSpec cluster, NodeType hostType) {
if ( hostType.isSharable())
- return nodeRepository.exclusiveAllocation(cluster) ? HostSharing.exclusive : HostSharing.any;
+ return cluster.isExclusive() ? HostSharing.provision :
+ nodeRepository.exclusiveAllocation(cluster) ? HostSharing.exclusive :
+ HostSharing.any;
else
return HostSharing.any;
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java
index 7da80440667..8a84cfef09a 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java
@@ -31,6 +31,7 @@ public class ProvisionedHost {
private final Flavor hostFlavor;
private final NodeType hostType;
private final Optional<ApplicationId> provisionedForApplicationId;
+ private final Optional<ApplicationId> exclusiveToApplicationId;
private final Optional<ClusterSpec.Type> exclusiveToClusterType;
private final List<HostName> nodeHostnames;
private final NodeResources nodeResources;
@@ -38,7 +39,9 @@ public class ProvisionedHost {
private final CloudAccount cloudAccount;
public ProvisionedHost(String id, String hostHostname, Flavor hostFlavor, NodeType hostType,
- Optional<ApplicationId> provisionedForApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType,
+ Optional<ApplicationId> provisionedForApplicationId,
+ Optional<ApplicationId> exclusiveToApplicationId,
+ Optional<ClusterSpec.Type> exclusiveToClusterType,
List<HostName> nodeHostnames, NodeResources nodeResources,
Version osVersion, CloudAccount cloudAccount) {
if (!hostType.isHost()) throw new IllegalArgumentException(hostType + " is not a host");
@@ -47,6 +50,7 @@ public class ProvisionedHost {
this.hostFlavor = Objects.requireNonNull(hostFlavor, "Host flavor must be set");
this.hostType = Objects.requireNonNull(hostType, "Host type must be set");
this.provisionedForApplicationId = Objects.requireNonNull(provisionedForApplicationId, "provisionedForApplicationId must be set");
+ this.exclusiveToApplicationId = Objects.requireNonNull(exclusiveToApplicationId, "exclusiveToApplicationId must be set");
this.exclusiveToClusterType = Objects.requireNonNull(exclusiveToClusterType, "exclusiveToClusterType must be set");
this.nodeHostnames = validateNodeAddresses(nodeHostnames);
this.nodeResources = Objects.requireNonNull(nodeResources, "Node resources must be set");
@@ -68,6 +72,7 @@ public class ProvisionedHost {
.status(Status.initial().withOsVersion(OsVersion.EMPTY.withCurrent(Optional.of(osVersion))))
.cloudAccount(cloudAccount);
provisionedForApplicationId.ifPresent(builder::provisionedForApplicationId);
+ exclusiveToApplicationId.ifPresent(builder::exclusiveToApplicationId);
exclusiveToClusterType.ifPresent(builder::exclusiveToClusterType);
if ( ! hostTTL.isZero()) builder.hostTTL(hostTTL);
return builder.build();
@@ -85,6 +90,7 @@ public class ProvisionedHost {
public Flavor hostFlavor() { return hostFlavor; }
public NodeType hostType() { return hostType; }
public Optional<ApplicationId> provisionedForApplicationId() { return provisionedForApplicationId; }
+ public Optional<ApplicationId> exclusiveToApplicationId() { return exclusiveToApplicationId; }
public Optional<ClusterSpec.Type> exclusiveToClusterType() { return exclusiveToClusterType; }
public List<HostName> nodeHostnames() { return nodeHostnames; }
public NodeResources nodeResources() { return nodeResources; }
@@ -103,6 +109,7 @@ public class ProvisionedHost {
hostFlavor.equals(that.hostFlavor) &&
hostType == that.hostType &&
provisionedForApplicationId.equals(that.provisionedForApplicationId) &&
+ exclusiveToApplicationId.equals(that.exclusiveToApplicationId) &&
exclusiveToClusterType.equals(that.exclusiveToClusterType) &&
nodeHostnames.equals(that.nodeHostnames) &&
nodeResources.equals(that.nodeResources) &&
@@ -112,7 +119,7 @@ public class ProvisionedHost {
@Override
public int hashCode() {
- return Objects.hash(id, hostHostname, hostFlavor, hostType, provisionedForApplicationId, exclusiveToClusterType, nodeHostnames, nodeResources, osVersion, cloudAccount);
+ return Objects.hash(id, hostHostname, hostFlavor, hostType, provisionedForApplicationId, exclusiveToApplicationId, exclusiveToClusterType, nodeHostnames, nodeResources, osVersion, cloudAccount);
}
@Override
@@ -123,8 +130,9 @@ public class ProvisionedHost {
", hostFlavor=" + hostFlavor +
", hostType=" + hostType +
", provisionedForApplicationId=" + provisionedForApplicationId +
+ ", exclusiveToApplicationId=" + exclusiveToApplicationId +
", exclusiveToClusterType=" + exclusiveToClusterType +
- ", nodeAddresses=" + nodeHostnames +
+ ", nodeHostnames=" + nodeHostnames +
", nodeResources=" + nodeResources +
", osVersion=" + osVersion +
", cloudAccount=" + cloudAccount +
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java
index def3e003ab3..f7710ca7019 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java
@@ -78,8 +78,8 @@ public class MockHostProvisioner implements HostProvisioner {
Flavor hostFlavor = hostFlavors.get(request.clusterType().orElse(ClusterSpec.Type.content));
if (hostFlavor == null)
hostFlavor = flavors.stream()
- .filter(f -> request.sharing() == HostSharing.exclusive ? compatible(f, request.resources())
- : satisfies(f, request.resources()))
+ .filter(f -> request.sharing().isExclusiveAllocation() ? compatible(f, request.resources())
+ : satisfies(f, request.resources()))
.filter(f -> realHostResourcesWithinLimits.test(f.resources()))
.findFirst()
.orElseThrow(() -> new NodeAllocationException("No host flavor matches " + request.resources(), true));
@@ -91,7 +91,8 @@ public class MockHostProvisioner implements HostProvisioner {
hostHostname,
hostFlavor,
request.type(),
- request.sharing() == HostSharing.exclusive ? Optional.of(request.owner()) : Optional.empty(),
+ request.sharing() == HostSharing.provision ? Optional.of(request.owner()) : Optional.empty(),
+ request.sharing().isExclusiveAllocation() ? Optional.of(request.owner()) : Optional.empty(),
Optional.empty(),
createHostnames(request.type(), hostFlavor, index),
request.resources(),
diff --git a/screwdriver.yaml b/screwdriver.yaml
index 6efb9145b09..a3eedc02999 100644
--- a/screwdriver.yaml
+++ b/screwdriver.yaml
@@ -34,7 +34,7 @@ shared:
du -sh /tmp/vespa/*
if [[ -z "$SD_PULL_REQUEST" ]]; then
- if [[ -z $VESPA_USE_SANITIZER ]] || [[ $VESPA_USE_SANITIZER == null ]]; then
+ if [[ -z "$VESPA_USE_SANITIZER" ]] || [[ "$VESPA_USE_SANITIZER" == null ]]; then
# Remove what we have produced
rm -rf $LOCAL_MVN_REPO/com/yahoo
rm -rf $LOCAL_MVN_REPO/ai/vespa
diff --git a/storage/src/tests/storageserver/documentapiconvertertest.cpp b/storage/src/tests/storageserver/documentapiconvertertest.cpp
index 5e70c00cc5f..eb4789b25d4 100644
--- a/storage/src/tests/storageserver/documentapiconvertertest.cpp
+++ b/storage/src/tests/storageserver/documentapiconvertertest.cpp
@@ -77,7 +77,7 @@ struct DocumentApiConverterTest : Test {
}
void SetUp() override {
- _converter = std::make_unique<DocumentApiConverter>(config::ConfigUri("raw:"), _bucketResolver);
+ _converter = std::make_unique<DocumentApiConverter>(_bucketResolver);
};
template <typename DerivedT, typename BaseT>
diff --git a/storage/src/tests/storageserver/priorityconvertertest.cpp b/storage/src/tests/storageserver/priorityconvertertest.cpp
index 5462c83d2a2..69f9d313242 100644
--- a/storage/src/tests/storageserver/priorityconvertertest.cpp
+++ b/storage/src/tests/storageserver/priorityconvertertest.cpp
@@ -1,7 +1,6 @@
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#include <vespa/storage/storageserver/priorityconverter.h>
-#include <tests/common/testhelper.h>
#include <vespa/vespalib/gtest/gtest.h>
using namespace ::testing;
@@ -12,8 +11,7 @@ struct PriorityConverterTest : Test {
std::unique_ptr<PriorityConverter> _converter;
void SetUp() override {
- vdstestlib::DirConfig config(getStandardConfig(true));
- _converter = std::make_unique<PriorityConverter>(config::ConfigUri(config.getConfigId()));
+ _converter = std::make_unique<PriorityConverter>();
};
};
diff --git a/storage/src/tests/storageserver/testvisitormessagesession.h b/storage/src/tests/storageserver/testvisitormessagesession.h
index 86d4dc92a58..cc7dab7ef9e 100644
--- a/storage/src/tests/storageserver/testvisitormessagesession.h
+++ b/storage/src/tests/storageserver/testvisitormessagesession.h
@@ -49,9 +49,11 @@ struct TestVisitorMessageSessionFactory : public VisitorMessageSessionFactory
bool _createAutoReplyVisitorSessions;
PriorityConverter _priConverter;
- TestVisitorMessageSessionFactory(vespalib::stringref configId = "")
+ TestVisitorMessageSessionFactory()
: _createAutoReplyVisitorSessions(false),
- _priConverter(config::ConfigUri(configId)) {}
+ _priConverter()
+ {
+ }
VisitorMessageSession::UP createSession(Visitor& v, VisitorThread& vt) override {
std::lock_guard lock(_accessLock);
diff --git a/storage/src/tests/visiting/visitormanagertest.cpp b/storage/src/tests/visiting/visitormanagertest.cpp
index 991b98e5489..2b7039f36ea 100644
--- a/storage/src/tests/visiting/visitormanagertest.cpp
+++ b/storage/src/tests/visiting/visitormanagertest.cpp
@@ -83,7 +83,7 @@ VisitorManagerTest::initializeTest(bool defer_manager_thread_start)
vdstestlib::DirConfig config(getStandardConfig(true));
config.getConfig("stor-visitor").set("visitorthreads", "1");
- _messageSessionFactory = std::make_unique<TestVisitorMessageSessionFactory>(config.getConfigId());
+ _messageSessionFactory = std::make_unique<TestVisitorMessageSessionFactory>();
_node = std::make_unique<TestServiceLayerApp>(config.getConfigId());
_node->setupDummyPersistence();
_node->getStateUpdater().setClusterState(std::make_shared<lib::ClusterState>("storage:1 distributor:1"));
diff --git a/storage/src/tests/visiting/visitortest.cpp b/storage/src/tests/visiting/visitortest.cpp
index 1f1d27ab4cb..49f1bc778fc 100644
--- a/storage/src/tests/visiting/visitortest.cpp
+++ b/storage/src/tests/visiting/visitortest.cpp
@@ -161,7 +161,7 @@ VisitorTest::initializeTest(const TestParams& params)
std::filesystem::create_directories(std::filesystem::path(vespalib::make_string("%s/disks/d0", rootFolder.c_str())));
std::filesystem::create_directories(std::filesystem::path(vespalib::make_string("%s/disks/d1", rootFolder.c_str())));
- _messageSessionFactory = std::make_unique<TestVisitorMessageSessionFactory>(config.getConfigId());
+ _messageSessionFactory = std::make_unique<TestVisitorMessageSessionFactory>();
if (params._autoReplyError.getCode() != mbus::ErrorCode::NONE) {
_messageSessionFactory->_autoReplyError = params._autoReplyError;
_messageSessionFactory->_createAutoReplyVisitorSessions = true;
diff --git a/storage/src/vespa/storage/storageserver/communicationmanager.cpp b/storage/src/vespa/storage/storageserver/communicationmanager.cpp
index c126ec01dc6..bbd4e87cb40 100644
--- a/storage/src/vespa/storage/storageserver/communicationmanager.cpp
+++ b/storage/src/vespa/storage/storageserver/communicationmanager.cpp
@@ -230,7 +230,7 @@ CommunicationManager::CommunicationManager(StorageComponentRegister& compReg,
_mbus(),
_configUri(configUri),
_closed(false),
- _docApiConverter(configUri, std::make_shared<PlaceHolderBucketResolver>()), // TODO wire config from outside
+ _docApiConverter(std::make_shared<PlaceHolderBucketResolver>()),
_thread()
{
_component.registerMetricUpdateHook(*this, 5s);
diff --git a/storage/src/vespa/storage/storageserver/documentapiconverter.cpp b/storage/src/vespa/storage/storageserver/documentapiconverter.cpp
index 04b3d8b6ce7..ca46e87285b 100644
--- a/storage/src/vespa/storage/storageserver/documentapiconverter.cpp
+++ b/storage/src/vespa/storage/storageserver/documentapiconverter.cpp
@@ -23,9 +23,8 @@ using document::BucketSpace;
namespace storage {
-DocumentApiConverter::DocumentApiConverter(const config::ConfigUri &configUri,
- std::shared_ptr<const BucketResolver> bucketResolver)
- : _priConverter(std::make_unique<PriorityConverter>(configUri)),
+DocumentApiConverter::DocumentApiConverter(std::shared_ptr<const BucketResolver> bucketResolver)
+ : _priConverter(std::make_unique<PriorityConverter>()),
_bucketResolver(std::move(bucketResolver))
{}
diff --git a/storage/src/vespa/storage/storageserver/documentapiconverter.h b/storage/src/vespa/storage/storageserver/documentapiconverter.h
index 5990d6f9017..96b119ff44e 100644
--- a/storage/src/vespa/storage/storageserver/documentapiconverter.h
+++ b/storage/src/vespa/storage/storageserver/documentapiconverter.h
@@ -22,18 +22,17 @@ class PriorityConverter;
class DocumentApiConverter
{
public:
- DocumentApiConverter(const config::ConfigUri &configUri,
- std::shared_ptr<const BucketResolver> bucketResolver);
+ explicit DocumentApiConverter(std::shared_ptr<const BucketResolver> bucketResolver);
~DocumentApiConverter();
- std::unique_ptr<api::StorageCommand> toStorageAPI(documentapi::DocumentMessage& msg);
- std::unique_ptr<api::StorageReply> toStorageAPI(documentapi::DocumentReply& reply, api::StorageCommand& originalCommand);
+ [[nodiscard]] std::unique_ptr<api::StorageCommand> toStorageAPI(documentapi::DocumentMessage& msg);
+ [[nodiscard]] std::unique_ptr<api::StorageReply> toStorageAPI(documentapi::DocumentReply& reply, api::StorageCommand& originalCommand);
void transferReplyState(storage::api::StorageReply& from, mbus::Reply& to);
- std::unique_ptr<mbus::Message> toDocumentAPI(api::StorageCommand& cmd);
+ [[nodiscard]] std::unique_ptr<mbus::Message> toDocumentAPI(api::StorageCommand& cmd);
const PriorityConverter& getPriorityConverter() const { return *_priConverter; }
// BucketResolver getter and setter are both thread safe.
- std::shared_ptr<const BucketResolver> bucketResolver() const;
+ [[nodiscard]] std::shared_ptr<const BucketResolver> bucketResolver() const;
void setBucketResolver(std::shared_ptr<const BucketResolver> resolver);
private:
mutable std::mutex _mutex;
diff --git a/storage/src/vespa/storage/storageserver/priorityconverter.cpp b/storage/src/vespa/storage/storageserver/priorityconverter.cpp
index 13ab572c561..fe7570ff53a 100644
--- a/storage/src/vespa/storage/storageserver/priorityconverter.cpp
+++ b/storage/src/vespa/storage/storageserver/priorityconverter.cpp
@@ -1,85 +1,91 @@
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#include "priorityconverter.h"
-#include <vespa/config/subscription/configuri.h>
-#include <vespa/config/helper/configfetcher.hpp>
-
+#include <map>
namespace storage {
-PriorityConverter::PriorityConverter(const config::ConfigUri & configUri)
- : _configFetcher(std::make_unique<config::ConfigFetcher>(configUri.getContext()))
+PriorityConverter::PriorityConverter()
+ : _mapping(),
+ _reverse_mapping()
{
- _configFetcher->subscribe<vespa::config::content::core::StorPrioritymappingConfig>(configUri.getConfigId(), this);
- _configFetcher->start();
+ init_static_priority_mappings();
}
PriorityConverter::~PriorityConverter() = default;
-uint8_t
-PriorityConverter::toStoragePriority(documentapi::Priority::Value documentApiPriority) const
+void
+PriorityConverter::init_static_priority_mappings()
{
- const uint32_t index(static_cast<uint32_t>(documentApiPriority));
- if (index >= PRI_ENUM_SIZE) {
- return 255;
- }
+ // Defaults from `stor-prioritymapping` config
+ constexpr uint8_t highest = 50;
+ constexpr uint8_t very_high = 60;
+ constexpr uint8_t high_1 = 70;
+ constexpr uint8_t high_2 = 80;
+ constexpr uint8_t high_3 = 90;
+ constexpr uint8_t normal_1 = 100;
+ constexpr uint8_t normal_2 = 110;
+ constexpr uint8_t normal_3 = 120;
+ constexpr uint8_t normal_4 = 130;
+ constexpr uint8_t normal_5 = 140;
+ constexpr uint8_t normal_6 = 150;
+ constexpr uint8_t low_1 = 160;
+ constexpr uint8_t low_2 = 170;
+ constexpr uint8_t low_3 = 180;
+ constexpr uint8_t very_low = 190;
+ constexpr uint8_t lowest = 200;
- return _mapping[index];
-}
+ _mapping[documentapi::Priority::PRI_HIGHEST] = highest;
+ _mapping[documentapi::Priority::PRI_VERY_HIGH] = very_high;
+ _mapping[documentapi::Priority::PRI_HIGH_1] = high_1;
+ _mapping[documentapi::Priority::PRI_HIGH_2] = high_2;
+ _mapping[documentapi::Priority::PRI_HIGH_3] = high_3;
+ _mapping[documentapi::Priority::PRI_NORMAL_1] = normal_1;
+ _mapping[documentapi::Priority::PRI_NORMAL_2] = normal_2;
+ _mapping[documentapi::Priority::PRI_NORMAL_3] = normal_3;
+ _mapping[documentapi::Priority::PRI_NORMAL_4] = normal_4;
+ _mapping[documentapi::Priority::PRI_NORMAL_5] = normal_5;
+ _mapping[documentapi::Priority::PRI_NORMAL_6] = normal_6;
+ _mapping[documentapi::Priority::PRI_LOW_1] = low_1;
+ _mapping[documentapi::Priority::PRI_LOW_2] = low_2;
+ _mapping[documentapi::Priority::PRI_LOW_3] = low_3;
+ _mapping[documentapi::Priority::PRI_VERY_LOW] = very_low;
+ _mapping[documentapi::Priority::PRI_LOWEST] = lowest;
-documentapi::Priority::Value
-PriorityConverter::toDocumentPriority(uint8_t storagePriority) const
-{
- std::lock_guard guard(_mutex);
- std::map<uint8_t, documentapi::Priority::Value>::const_iterator iter =
- _reverseMapping.lower_bound(storagePriority);
+ std::map<uint8_t, documentapi::Priority::Value> reverse_map_helper;
+ reverse_map_helper[highest] = documentapi::Priority::PRI_HIGHEST;
+ reverse_map_helper[very_high] = documentapi::Priority::PRI_VERY_HIGH;
+ reverse_map_helper[high_1] = documentapi::Priority::PRI_HIGH_1;
+ reverse_map_helper[high_2] = documentapi::Priority::PRI_HIGH_2;
+ reverse_map_helper[high_3] = documentapi::Priority::PRI_HIGH_3;
+ reverse_map_helper[normal_1] = documentapi::Priority::PRI_NORMAL_1;
+ reverse_map_helper[normal_2] = documentapi::Priority::PRI_NORMAL_2;
+ reverse_map_helper[normal_3] = documentapi::Priority::PRI_NORMAL_3;
+ reverse_map_helper[normal_4] = documentapi::Priority::PRI_NORMAL_4;
+ reverse_map_helper[normal_5] = documentapi::Priority::PRI_NORMAL_5;
+ reverse_map_helper[normal_6] = documentapi::Priority::PRI_NORMAL_6;
+ reverse_map_helper[low_1] = documentapi::Priority::PRI_LOW_1;
+ reverse_map_helper[low_2] = documentapi::Priority::PRI_LOW_2;
+ reverse_map_helper[low_3] = documentapi::Priority::PRI_LOW_3;
+ reverse_map_helper[very_low] = documentapi::Priority::PRI_VERY_LOW;
+ reverse_map_helper[lowest] = documentapi::Priority::PRI_LOWEST;
- if (iter != _reverseMapping.end()) {
- return iter->second;
+ // Precompute a 1-1 LUT to avoid having to lower-bound lookup values in a fixed map
+ _reverse_mapping.resize(256);
+ for (size_t i = 0; i < 256; ++i) {
+ auto iter = reverse_map_helper.lower_bound(static_cast<uint8_t>(i));
+ _reverse_mapping[i] = (iter != reverse_map_helper.cend()) ? iter->second : documentapi::Priority::PRI_LOWEST;
}
-
- return documentapi::Priority::PRI_LOWEST;
}
-void
-PriorityConverter::configure(std::unique_ptr<vespa::config::content::core::StorPrioritymappingConfig> config)
+uint8_t
+PriorityConverter::toStoragePriority(documentapi::Priority::Value documentApiPriority) const noexcept
{
- // Data race free; _mapping is an array of std::atomic.
- _mapping[documentapi::Priority::PRI_HIGHEST] = config->highest;
- _mapping[documentapi::Priority::PRI_VERY_HIGH] = config->veryHigh;
- _mapping[documentapi::Priority::PRI_HIGH_1] = config->high1;
- _mapping[documentapi::Priority::PRI_HIGH_2] = config->high2;
- _mapping[documentapi::Priority::PRI_HIGH_3] = config->high3;
- _mapping[documentapi::Priority::PRI_NORMAL_1] = config->normal1;
- _mapping[documentapi::Priority::PRI_NORMAL_2] = config->normal2;
- _mapping[documentapi::Priority::PRI_NORMAL_3] = config->normal3;
- _mapping[documentapi::Priority::PRI_NORMAL_4] = config->normal4;
- _mapping[documentapi::Priority::PRI_NORMAL_5] = config->normal5;
- _mapping[documentapi::Priority::PRI_NORMAL_6] = config->normal6;
- _mapping[documentapi::Priority::PRI_LOW_1] = config->low1;
- _mapping[documentapi::Priority::PRI_LOW_2] = config->low2;
- _mapping[documentapi::Priority::PRI_LOW_3] = config->low3;
- _mapping[documentapi::Priority::PRI_VERY_LOW] = config->veryLow;
- _mapping[documentapi::Priority::PRI_LOWEST] = config->lowest;
-
- std::lock_guard guard(_mutex);
- _reverseMapping.clear();
- _reverseMapping[config->highest] = documentapi::Priority::PRI_HIGHEST;
- _reverseMapping[config->veryHigh] = documentapi::Priority::PRI_VERY_HIGH;
- _reverseMapping[config->high1] = documentapi::Priority::PRI_HIGH_1;
- _reverseMapping[config->high2] = documentapi::Priority::PRI_HIGH_2;
- _reverseMapping[config->high3] = documentapi::Priority::PRI_HIGH_3;
- _reverseMapping[config->normal1] = documentapi::Priority::PRI_NORMAL_1;
- _reverseMapping[config->normal2] = documentapi::Priority::PRI_NORMAL_2;
- _reverseMapping[config->normal3] = documentapi::Priority::PRI_NORMAL_3;
- _reverseMapping[config->normal4] = documentapi::Priority::PRI_NORMAL_4;
- _reverseMapping[config->normal5] = documentapi::Priority::PRI_NORMAL_5;
- _reverseMapping[config->normal6] = documentapi::Priority::PRI_NORMAL_6;
- _reverseMapping[config->low1] = documentapi::Priority::PRI_LOW_1;
- _reverseMapping[config->low2] = documentapi::Priority::PRI_LOW_2;
- _reverseMapping[config->low3] = documentapi::Priority::PRI_LOW_3;
- _reverseMapping[config->veryLow] = documentapi::Priority::PRI_VERY_LOW;
- _reverseMapping[config->lowest] = documentapi::Priority::PRI_LOWEST;
+ const auto index = static_cast<uint32_t>(documentApiPriority);
+ if (index >= PRI_ENUM_SIZE) {
+ return 255;
+ }
+ return _mapping[index];
}
} // storage
diff --git a/storage/src/vespa/storage/storageserver/priorityconverter.h b/storage/src/vespa/storage/storageserver/priorityconverter.h
index 47326e54243..48c7424433b 100644
--- a/storage/src/vespa/storage/storageserver/priorityconverter.h
+++ b/storage/src/vespa/storage/storageserver/priorityconverter.h
@@ -2,50 +2,34 @@
#pragma once
-#include <vespa/storage/config/config-stor-prioritymapping.h>
-#include <vespa/config/helper/ifetchercallback.h>
#include <vespa/documentapi/messagebus/priority.h>
-#include <atomic>
#include <array>
-#include <mutex>
-
-namespace config {
- class ConfigUri;
- class ConfigFetcher;
-}
+#include <vector>
namespace storage {
-class PriorityConverter
- : public config::IFetcherCallback<
- vespa::config::content::core::StorPrioritymappingConfig>
-{
+class PriorityConverter {
public:
- using Config = vespa::config::content::core::StorPrioritymappingConfig;
-
- explicit PriorityConverter(const config::ConfigUri& configUri);
- ~PriorityConverter() override;
+ PriorityConverter();
+ ~PriorityConverter();
/** Converts the given priority into a storage api priority number. */
- uint8_t toStoragePriority(documentapi::Priority::Value) const;
+ [[nodiscard]] uint8_t toStoragePriority(documentapi::Priority::Value) const noexcept;
/** Converts the given priority into a document api priority number. */
- documentapi::Priority::Value toDocumentPriority(uint8_t) const;
-
- void configure(std::unique_ptr<Config> config) override;
+ [[nodiscard]] documentapi::Priority::Value toDocumentPriority(uint8_t storage_priority) const noexcept {
+ return _reverse_mapping[storage_priority];
+ }
private:
- static_assert(documentapi::Priority::PRI_ENUM_SIZE == 16,
- "Unexpected size of priority enumeration");
- static_assert(documentapi::Priority::PRI_LOWEST == 15,
- "Priority enum value out of bounds");
- static constexpr size_t PRI_ENUM_SIZE = documentapi::Priority::PRI_ENUM_SIZE;
+ void init_static_priority_mappings();
- std::array<std::atomic<uint8_t>, PRI_ENUM_SIZE> _mapping;
- std::map<uint8_t, documentapi::Priority::Value> _reverseMapping;
- mutable std::mutex _mutex;
+ static_assert(documentapi::Priority::PRI_ENUM_SIZE == 16, "Unexpected size of priority enumeration");
+ static_assert(documentapi::Priority::PRI_LOWEST == 15, "Priority enum value out of bounds");
+ static constexpr size_t PRI_ENUM_SIZE = documentapi::Priority::PRI_ENUM_SIZE;
- std::unique_ptr<config::ConfigFetcher> _configFetcher;
+ std::array<uint8_t, PRI_ENUM_SIZE> _mapping;
+ std::vector<documentapi::Priority::Value> _reverse_mapping;
};
} // storage
diff --git a/vespajlib/abi-spec.json b/vespajlib/abi-spec.json
index 1c19c2ba5d6..3e588e24d47 100644
--- a/vespajlib/abi-spec.json
+++ b/vespajlib/abi-spec.json
@@ -4045,7 +4045,8 @@
"abstract"
],
"methods" : [
- "public abstract java.util.List complete(ai.vespa.llm.completion.Prompt)"
+ "public abstract java.util.List complete(ai.vespa.llm.completion.Prompt)",
+ "public abstract java.util.concurrent.CompletableFuture completeAsync(ai.vespa.llm.completion.Prompt, java.util.function.Consumer)"
],
"fields" : [ ]
},
@@ -4059,6 +4060,7 @@
"public void <init>(java.lang.String)",
"public ai.vespa.llm.client.openai.OpenAiClient$Builder model(java.lang.String)",
"public ai.vespa.llm.client.openai.OpenAiClient$Builder temperature(double)",
+ "public ai.vespa.llm.client.openai.OpenAiClient$Builder maxTokens(long)",
"public ai.vespa.llm.client.openai.OpenAiClient build()"
],
"fields" : [ ]
@@ -4072,7 +4074,8 @@
"public"
],
"methods" : [
- "public java.util.List complete(ai.vespa.llm.completion.Prompt)"
+ "public java.util.List complete(ai.vespa.llm.completion.Prompt)",
+ "public java.util.concurrent.CompletableFuture completeAsync(ai.vespa.llm.completion.Prompt, java.util.function.Consumer)"
],
"fields" : [ ]
},
@@ -4090,7 +4093,8 @@
],
"fields" : [
"public static final enum ai.vespa.llm.completion.Completion$FinishReason length",
- "public static final enum ai.vespa.llm.completion.Completion$FinishReason stop"
+ "public static final enum ai.vespa.llm.completion.Completion$FinishReason stop",
+ "public static final enum ai.vespa.llm.completion.Completion$FinishReason none"
]
},
"ai.vespa.llm.completion.Completion" : {
@@ -4167,7 +4171,8 @@
],
"methods" : [
"public void <init>(ai.vespa.llm.test.MockLanguageModel$Builder)",
- "public java.util.List complete(ai.vespa.llm.completion.Prompt)"
+ "public java.util.List complete(ai.vespa.llm.completion.Prompt)",
+ "public java.util.concurrent.CompletableFuture completeAsync(ai.vespa.llm.completion.Prompt, java.util.function.Consumer)"
],
"fields" : [ ]
}
diff --git a/vespajlib/src/main/java/ai/vespa/llm/LanguageModel.java b/vespajlib/src/main/java/ai/vespa/llm/LanguageModel.java
index bd9004a659b..f4b8938934b 100644
--- a/vespajlib/src/main/java/ai/vespa/llm/LanguageModel.java
+++ b/vespajlib/src/main/java/ai/vespa/llm/LanguageModel.java
@@ -6,6 +6,8 @@ import ai.vespa.llm.completion.Prompt;
import com.yahoo.api.annotations.Beta;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
/**
* Interface to language models.
@@ -17,4 +19,6 @@ public interface LanguageModel {
List<Completion> complete(Prompt prompt);
+ CompletableFuture<Completion.FinishReason> completeAsync(Prompt prompt, Consumer<Completion> action);
+
}
diff --git a/vespajlib/src/main/java/ai/vespa/llm/client/openai/OpenAiClient.java b/vespajlib/src/main/java/ai/vespa/llm/client/openai/OpenAiClient.java
index efa8927988c..d7334b40963 100644
--- a/vespajlib/src/main/java/ai/vespa/llm/client/openai/OpenAiClient.java
+++ b/vespajlib/src/main/java/ai/vespa/llm/client/openai/OpenAiClient.java
@@ -18,25 +18,34 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* A client to the OpenAI language model API. Refer to https://platform.openai.com/docs/api-reference/.
- * Currently only completions are implemented.
+ * Currently, only completions are implemented.
*
* @author bratseth
*/
@Beta
public class OpenAiClient implements LanguageModel {
+ private static final String DATA_FIELD = "data: ";
+
private final String token;
private final String model;
private final double temperature;
+ private final long maxTokens;
+
private final HttpClient httpClient;
private OpenAiClient(Builder builder) {
this.token = builder.token;
this.model = builder.model;
this.temperature = builder.temperature;
+ this.maxTokens = builder.maxTokens;
this.httpClient = HttpClient.newBuilder().build();
}
@@ -54,13 +63,63 @@ public class OpenAiClient implements LanguageModel {
}
}
+ @Override
+ public CompletableFuture<Completion.FinishReason> completeAsync(Prompt prompt, Consumer<Completion> consumer) {
+ try {
+ var request = toRequest(prompt, true);
+ var futureResponse = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofLines());
+ var completionFuture = new CompletableFuture<Completion.FinishReason>();
+
+ futureResponse.thenAcceptAsync(response -> {
+ try {
+ int responseCode = response.statusCode();
+ if (responseCode != 200) {
+ throw new IllegalArgumentException("Received code " + responseCode + ": " +
+ response.body().collect(Collectors.joining()));
+ }
+
+ Stream<String> lines = response.body();
+ lines.forEach(line -> {
+ if (line.startsWith(DATA_FIELD)) {
+ var root = SlimeUtils.jsonToSlime(line.substring(DATA_FIELD.length())).get();
+ var completion = toCompletions(root, "delta").get(0);
+ consumer.accept(completion);
+ if (!completion.finishReason().equals(Completion.FinishReason.none)) {
+ completionFuture.complete(completion.finishReason());
+ }
+ }
+ });
+ } catch (Exception e) {
+ completionFuture.completeExceptionally(e);
+ }
+ });
+ return completionFuture;
+
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
private HttpRequest toRequest(Prompt prompt) throws IOException, URISyntaxException {
+ return toRequest(prompt, false);
+ }
+
+ private HttpRequest toRequest(Prompt prompt, boolean stream) throws IOException, URISyntaxException {
var slime = new Slime();
var root = slime.setObject();
root.setString("model", model);
root.setDouble("temperature", temperature);
- root.setString("prompt", prompt.asString());
- return HttpRequest.newBuilder(new URI("https://api.openai.com/v1/completions"))
+ root.setBool("stream", stream);
+ root.setLong("n", 1);
+ if (maxTokens > 0) {
+ root.setLong("max_tokens", maxTokens);
+ }
+ var messagesArray = root.setArray("messages");
+ var messagesObject = messagesArray.addObject();
+ messagesObject.setString("role", "user");
+ messagesObject.setString("content", prompt.asString());
+
+ return HttpRequest.newBuilder(new URI("https://api.openai.com/v1/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.POST(HttpRequest.BodyPublishers.ofByteArray(SlimeUtils.toJsonBytes(slime)))
@@ -68,21 +127,27 @@ public class OpenAiClient implements LanguageModel {
}
private List<Completion> toCompletions(Inspector response) {
+ return toCompletions(response, "message");
+ }
+
+ private List<Completion> toCompletions(Inspector response, String field) {
List<Completion> completions = new ArrayList<>();
response.field("choices")
- .traverse((ArrayTraverser) (__, choice) -> completions.add(toCompletion(choice)));
+ .traverse((ArrayTraverser) (__, choice) -> completions.add(toCompletion(choice, field)));
return completions;
}
- private Completion toCompletion(Inspector choice) {
- return new Completion(choice.field("text").asString(),
- toFinishReason(choice.field("finish_reason").asString()));
+ private Completion toCompletion(Inspector choice, String field) {
+ var content = choice.field(field).field("content").asString();
+ var finishReason = toFinishReason(choice.field("finish_reason").asString());
+ return new Completion(content, finishReason);
}
private Completion.FinishReason toFinishReason(String finishReasonString) {
return switch(finishReasonString) {
case "length" -> Completion.FinishReason.length;
case "stop" -> Completion.FinishReason.stop;
+ case "", "null" -> Completion.FinishReason.none;
default -> throw new IllegalStateException("Unknown OpenAi completion finish reason '" + finishReasonString + "'");
};
}
@@ -90,8 +155,9 @@ public class OpenAiClient implements LanguageModel {
public static class Builder {
private final String token;
- private String model = "text-davinci-003";
- private double temperature = 0;
+ private String model = "gpt-3.5-turbo";
+ private double temperature = 0.0;
+ private long maxTokens = 0;
public Builder(String token) {
this.token = token;
@@ -109,6 +175,12 @@ public class OpenAiClient implements LanguageModel {
return this;
}
+ /** Maximum number of tokens to generate */
+ public Builder maxTokens(long maxTokens) {
+ this.maxTokens = maxTokens;
+ return this;
+ }
+
public OpenAiClient build() {
return new OpenAiClient(this);
}
diff --git a/vespajlib/src/main/java/ai/vespa/llm/completion/Completion.java b/vespajlib/src/main/java/ai/vespa/llm/completion/Completion.java
index f5731852d93..ea784013812 100644
--- a/vespajlib/src/main/java/ai/vespa/llm/completion/Completion.java
+++ b/vespajlib/src/main/java/ai/vespa/llm/completion/Completion.java
@@ -19,8 +19,10 @@ public record Completion(String text, FinishReason finishReason) {
length,
/** The completion is the predicted ending of the prompt. */
- stop
+ stop,
+ /** The completion is not finished yet, more tokens are incoming. */
+ none
}
public Completion(String text, FinishReason finishReason) {
diff --git a/vespajlib/src/main/java/ai/vespa/llm/test/MockLanguageModel.java b/vespajlib/src/main/java/ai/vespa/llm/test/MockLanguageModel.java
index 16e9c4e1848..db1b42fbbac 100644
--- a/vespajlib/src/main/java/ai/vespa/llm/test/MockLanguageModel.java
+++ b/vespajlib/src/main/java/ai/vespa/llm/test/MockLanguageModel.java
@@ -7,6 +7,8 @@ import ai.vespa.llm.completion.Prompt;
import com.yahoo.api.annotations.Beta;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
import java.util.function.Function;
/**
@@ -26,6 +28,11 @@ public class MockLanguageModel implements LanguageModel {
return completer.apply(prompt);
}
+ @Override
+ public CompletableFuture<Completion.FinishReason> completeAsync(Prompt prompt, Consumer<Completion> action) {
+ throw new RuntimeException("Not implemented");
+ }
+
public static class Builder {
private Function<Prompt, List<Completion>> completer = prompt -> List.of(Completion.from(""));
diff --git a/vespajlib/src/test/java/ai/vespa/llm/client/openai/OpenAiClientCompletionTest.java b/vespajlib/src/test/java/ai/vespa/llm/client/openai/OpenAiClientCompletionTest.java
index 444f082b1c0..45ef7e270aa 100644
--- a/vespajlib/src/test/java/ai/vespa/llm/client/openai/OpenAiClientCompletionTest.java
+++ b/vespajlib/src/test/java/ai/vespa/llm/client/openai/OpenAiClientCompletionTest.java
@@ -11,12 +11,14 @@ import org.junit.jupiter.api.Test;
*/
public class OpenAiClientCompletionTest {
+ private static final String apiKey = "your-api-key-here";
+
@Test
@Disabled
public void testClient() {
- var client = new OpenAiClient.Builder("your token here").build();
+ var client = new OpenAiClient.Builder(apiKey).maxTokens(10).build();
String input = "You are an unhelpful assistant who never answers questions straightforwardly. " +
- "Be as long-winded as possible. Are humans smarter than cats?";
+ "Be as long-winded as possible. Are humans smarter than cats?\n\n";
StringPrompt prompt = StringPrompt.from(input);
System.out.print(prompt);
for (int i = 0; i < 10; i++) {
@@ -27,4 +29,19 @@ public class OpenAiClientCompletionTest {
}
}
+ @Test
+ @Disabled
+ public void testAsyncClient() {
+ var client = new OpenAiClient.Builder(apiKey).build();
+ String input = "You are an unhelpful assistant who never answers questions straightforwardly. " +
+ "Be as long-winded as possible. Are humans smarter than cats?\n\n";
+ StringPrompt prompt = StringPrompt.from(input);
+ System.out.print(prompt);
+ var future = client.completeAsync(prompt, completion -> {
+ System.out.print(completion.text());
+ });
+ System.out.println("Waiting for completion...");
+ System.out.println("\nFinished streaming because of " + future.join());
+ }
+
}
diff --git a/vespalib/src/vespa/fastos/linux_file.cpp b/vespalib/src/vespa/fastos/linux_file.cpp
index b6094a050d9..0f32aa953a8 100644
--- a/vespalib/src/vespa/fastos/linux_file.cpp
+++ b/vespalib/src/vespa/fastos/linux_file.cpp
@@ -202,7 +202,7 @@ FastOS_Linux_File::Write2(const void *buffer, size_t length)
if (writtenNow > 0) {
written += writtenNow;
} else {
- return (written > 0) ? written : writtenNow;;
+ return (written > 0) ? written : writtenNow;
}
}
return written;
@@ -239,8 +239,8 @@ FastOS_Linux_File::internalWrite2(const void *buffer, size_t length)
}
if (writeRes > 0) {
_filePointer += writeRes;
- if (_filePointer > _cachedSize) {
- _cachedSize = _filePointer;
+ if (_filePointer > _cachedSize.load(std::memory_order_relaxed)) {
+ _cachedSize.store(_filePointer, std::memory_order_relaxed);
}
}
} else {
@@ -277,7 +277,7 @@ FastOS_Linux_File::SetSize(int64_t newSize)
bool rc = FastOS_UNIX_File::SetSize(newSize);
if (rc) {
- _cachedSize = newSize;
+ _cachedSize.store(newSize, std::memory_order_relaxed);
}
return rc;
}
@@ -334,19 +334,21 @@ FastOS_Linux_File::DirectIOPadding (int64_t offset, size_t length, size_t &padBe
if (padAfter == _directIOFileAlign) {
padAfter = 0;
}
- if (int64_t(offset+length+padAfter) > _cachedSize) {
+ int64_t fileSize = _cachedSize.load(std::memory_order_relaxed);
+ if (int64_t(offset+length+padAfter) > fileSize) {
// _cachedSize is not really trustworthy, so if we suspect it is not correct, we correct it.
// The main reason is that it will not reflect the file being extended by another filedescriptor.
- _cachedSize = getSize();
+ fileSize = getSize();
+ _cachedSize.store(fileSize, std::memory_order_relaxed);
}
if ((padAfter != 0) &&
- (static_cast<int64_t>(offset + length + padAfter) > _cachedSize) &&
- (static_cast<int64_t>(offset + length) <= _cachedSize))
+ (static_cast<int64_t>(offset + length + padAfter) > fileSize) &&
+ (static_cast<int64_t>(offset + length) <= fileSize))
{
- padAfter = _cachedSize - (offset + length);
+ padAfter = fileSize - (offset + length);
}
- if (static_cast<uint64_t>(offset + length + padAfter) <= static_cast<uint64_t>(_cachedSize)) {
+ if (static_cast<uint64_t>(offset + length + padAfter) <= static_cast<uint64_t>(fileSize)) {
return true;
}
}
diff --git a/vespalib/src/vespa/fastos/linux_file.h b/vespalib/src/vespa/fastos/linux_file.h
index 1295ce38316..af6e6af51af 100644
--- a/vespalib/src/vespa/fastos/linux_file.h
+++ b/vespalib/src/vespa/fastos/linux_file.h
@@ -10,21 +10,23 @@
#pragma once
#include "unix_file.h"
+#include <atomic>
/**
* This is the Linux implementation of @ref FastOS_File. Most
* methods are inherited from @ref FastOS_UNIX_File.
*/
-class FastOS_Linux_File : public FastOS_UNIX_File
+class FastOS_Linux_File final : public FastOS_UNIX_File
{
public:
using FastOS_UNIX_File::ReadBuf;
protected:
- int64_t _cachedSize;
+ std::atomic<int64_t> _cachedSize;
int64_t _filePointer; // Only maintained/used in directio mode
public:
- FastOS_Linux_File (const char *filename = nullptr);
+ FastOS_Linux_File() : FastOS_Linux_File(nullptr) {}
+ explicit FastOS_Linux_File(const char *filename);
~FastOS_Linux_File () override;
bool GetDirectIORestrictions(size_t &memoryAlignment, size_t &transferGranularity, size_t &transferMaximum) override;
bool DirectIOPadding(int64_t offset, size_t length, size_t &padBefore, size_t &padAfter) override;