summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorValerij Fredriksen <valerijf@verizonmedia.com>2020-01-17 11:57:03 +0100
committerValerij Fredriksen <valerijf@verizonmedia.com>2020-01-17 11:57:03 +0100
commit056b212aea1e1afe645239ef16924cc7002ef4f2 (patch)
tree7ebaab50ad542f96b4cd13b526aebd7b0788d110
parent97b1bdc542a0b7f33070f4f7efe93b5eef75b1a8 (diff)
Add large resource reduction validation check
-rw-r--r--config-model-api/abi-spec.json3
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java1
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java2
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ResourcesReductionValidator.java77
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ResourcesReductionValidatorTest.java103
5 files changed, 185 insertions, 1 deletions
diff --git a/config-model-api/abi-spec.json b/config-model-api/abi-spec.json
index 25f35781a1f..6b466c65cdb 100644
--- a/config-model-api/abi-spec.json
+++ b/config-model-api/abi-spec.json
@@ -531,6 +531,7 @@
"public static final enum com.yahoo.config.application.api.ValidationId indexModeChange",
"public static final enum com.yahoo.config.application.api.ValidationId fieldTypeChange",
"public static final enum com.yahoo.config.application.api.ValidationId clusterSizeReduction",
+ "public static final enum com.yahoo.config.application.api.ValidationId resourcesReduction",
"public static final enum com.yahoo.config.application.api.ValidationId contentTypeRemoval",
"public static final enum com.yahoo.config.application.api.ValidationId contentClusterRemoval",
"public static final enum com.yahoo.config.application.api.ValidationId deploymentRemoval",
@@ -605,4 +606,4 @@
"public static final com.yahoo.config.application.api.ValidationOverrides all"
]
}
-}
+} \ No newline at end of file
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java
index 35ece71a72e..c0bae137b0d 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java
@@ -14,6 +14,7 @@ public enum ValidationId {
indexModeChange("indexing-mode-change"), // Changing the index mode (streaming, indexed, store-only) of documents
fieldTypeChange("field-type-change"), // Field type changes
clusterSizeReduction("cluster-size-reduction"), // Large reductions in cluster size
+ resourcesReduction("resources-reduction"), // Large reductions in node resources
contentTypeRemoval("content-type-removal"), // Removal of a data type (causes deletion of all data)
contentClusterRemoval("content-cluster-removal"), // Removal (or id change) of content clusters
deploymentRemoval("deployment-removal"), // Removal of production zones from deployment.xml
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java
index 4959970d98e..8eabc61f71f 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java
@@ -17,6 +17,7 @@ import com.yahoo.vespa.model.application.validation.change.ContentTypeRemovalVal
import com.yahoo.vespa.model.application.validation.change.GlobalDocumentChangeValidator;
import com.yahoo.vespa.model.application.validation.change.IndexedSearchClusterChangeValidator;
import com.yahoo.vespa.model.application.validation.change.IndexingModeChangeValidator;
+import com.yahoo.vespa.model.application.validation.change.ResourcesReductionValidator;
import com.yahoo.vespa.model.application.validation.change.StartupCommandChangeValidator;
import com.yahoo.vespa.model.application.validation.change.StreamingSearchClusterChangeValidator;
import com.yahoo.vespa.model.application.validation.first.AccessControlOnFirstDeploymentValidator;
@@ -84,6 +85,7 @@ public class Validation {
new ContentTypeRemovalValidator(),
new ContentClusterRemovalValidator(),
new ClusterSizeReductionValidator(),
+ new ResourcesReductionValidator(),
new ContainerRestartValidator(),
};
return Arrays.stream(validators)
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ResourcesReductionValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ResourcesReductionValidator.java
new file mode 100644
index 00000000000..f14ad9c51a5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ResourcesReductionValidator.java
@@ -0,0 +1,77 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.config.application.api.ValidationId;
+import com.yahoo.config.application.api.ValidationOverrides;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.VespaModel;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Checks that no nodes resources are reduced too much in one go.
+ *
+ * @author freva
+ */
+public class ResourcesReductionValidator implements ChangeValidator {
+
+ @Override
+ public List<ConfigChangeAction> validate(VespaModel current, VespaModel next, ValidationOverrides overrides, Instant now) {
+ var currentRequestedResourcesByClusterId = getRequestedResourcesByClusterId(current);
+ var nextRequestedResourcesByClusterId = getRequestedResourcesByClusterId(next);
+
+ for (var clusterTypeAndId : currentRequestedResourcesByClusterId.keySet()) {
+ if (!nextRequestedResourcesByClusterId.containsKey(clusterTypeAndId)) continue;
+ validate(currentRequestedResourcesByClusterId.get(clusterTypeAndId),
+ nextRequestedResourcesByClusterId.get(clusterTypeAndId),
+ clusterTypeAndId.getSecond(),
+ overrides,
+ now);
+ }
+
+ return List.of();
+ }
+
+ private void validate(NodeResources currentResources, NodeResources nextResources, ClusterSpec.Id clusterId,
+ ValidationOverrides overrides, Instant now) {
+ List<String> illegalChanges = Stream.of(
+ validateResource("vCPU", currentResources.vcpu(), nextResources.vcpu()),
+ validateResource("memory GB", currentResources.memoryGb(), nextResources.memoryGb()),
+ validateResource("disk GB", currentResources.diskGb(), nextResources.diskGb()))
+ .flatMap(Optional::stream)
+ .collect(Collectors.toList());
+ if (illegalChanges.isEmpty()) return;
+
+ overrides.invalid(ValidationId.resourcesReduction,
+ "Resource reduction in '" + clusterId.value() + "' is too large. " +
+ String.join(" ", illegalChanges) + " New resources must be at least 50% of the current resources",
+ now);
+ }
+
+ private static Optional<String> validateResource(String resourceName, double currentValue, double nextValue) {
+ // don't allow more than 50% reduction, but always allow to reduce by 1
+ if (nextValue >= currentValue * 0.5 || nextValue >= currentValue - 1) return Optional.empty();
+ return Optional.of(String.format("Current %s: %.2f, new: %.2f.", resourceName, currentValue, nextValue));
+ }
+
+ private static Map<Pair<ClusterSpec.Type, ClusterSpec.Id>, NodeResources> getRequestedResourcesByClusterId(VespaModel vespaModel) {
+ return vespaModel.hostSystem().getHosts().stream()
+ .map(HostResource::spec)
+ .filter(spec -> spec.membership().isPresent() && spec.requestedResources().isPresent())
+ .filter(spec -> !spec.membership().get().retired())
+ .collect(Collectors.toMap(
+ spec -> new Pair<>(spec.membership().get().cluster().type(), spec.membership().get().cluster().id()),
+ spec -> spec.requestedResources().get(),
+ (e1, e2) -> e1));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ResourcesReductionValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ResourcesReductionValidatorTest.java
new file mode 100644
index 00000000000..1322a9061ed
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ResourcesReductionValidatorTest.java
@@ -0,0 +1,103 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.application.api.ValidationId;
+import com.yahoo.config.application.api.ValidationOverrides;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationTester;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author freva
+ */
+public class ResourcesReductionValidatorTest {
+
+ private final InMemoryProvisioner provisioner = new InMemoryProvisioner(30, new NodeResources(64, 128, 1000, 10));
+ private final ValidationTester tester = new ValidationTester(provisioner);
+
+ @Test
+ public void fail_when_reduction_by_over_50_percent() {
+ VespaModel previous = tester.deploy(null, getServices(new NodeResources(8, 64, 800, 1)), Environment.prod, null).getFirst();
+ try {
+ tester.deploy(previous, getServices(new NodeResources(8, 16, 800, 1)), Environment.prod, null);
+ fail("Expected exception due to resources reduction");
+ } catch (IllegalArgumentException expected) {
+ assertEquals("resources-reduction: Resource reduction in 'default' is too large. " +
+ "Current memory GB: 64.00, new: 16.00. New resources must be at least 50% of the current resources. " +
+ ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction),
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+ @Test
+ public void fail_when_reducing_multiple_resources_by_over_50_percent() {
+ VespaModel previous = tester.deploy(null, getServices(new NodeResources(8, 64, 800, 1)), Environment.prod, null).getFirst();
+ try {
+ tester.deploy(previous, getServices(new NodeResources(3, 16, 200, 1)), Environment.prod, null);
+ fail("Expected exception due to resources reduction");
+ } catch (IllegalArgumentException expected) {
+ assertEquals("resources-reduction: Resource reduction in 'default' is too large. " +
+ "Current vCPU: 8.00, new: 3.00. Current memory GB: 64.00, new: 16.00. Current disk GB: 800.00, new: 200.00. " +
+ "New resources must be at least 50% of the current resources. " +
+ ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction),
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+ @Test
+ public void small_resource_decrease_is_allowed() {
+ VespaModel previous = tester.deploy(null, getServices(new NodeResources(1.5, 64, 800, 1)), Environment.prod, null).getFirst();
+ tester.deploy(previous, getServices(new NodeResources(.5, 48, 600, 1)), Environment.prod, null);
+ }
+
+ @Test
+ public void overriding_resource_decrease() {
+ VespaModel previous = tester.deploy(null, getServices(new NodeResources(8, 64, 800, 1)), Environment.prod, null).getFirst();
+ tester.deploy(previous, getServices(new NodeResources(8, 16, 800, 1)), Environment.prod, resourcesReductionOverride); // Allowed due to override
+ }
+
+ @Test
+ public void allowed_to_go_to_not_specifying_resources() {
+ VespaModel previous = tester.deploy(null, getServices(new NodeResources(1.5, 64, 800, 1)), Environment.prod, null).getFirst();
+ tester.deploy(previous, getServices(null), Environment.prod, null);
+ }
+
+ @Test
+ public void allowed_to_go_from_not_specifying_resources() {
+ VespaModel previous = tester.deploy(null, getServices(null), Environment.prod, null).getFirst();
+ tester.deploy(previous, getServices(new NodeResources(1.5, 64, 800, 1)), Environment.prod, null);
+ }
+
+ private static String getServices(NodeResources resources) {
+ String resourcesStr = resources == null ?
+ "" :
+ String.format(" <resources vcpu='%.0f' memory='%.0fG' disk='%.0fG'/>",
+ resources.vcpu(), resources.memoryGb(), resources.diskGb());
+ return "<services version='1.0'>" +
+ " <content id='default' version='1.0'>" +
+ " <redundancy>1</redundancy>" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='5'>" +
+ resourcesStr +
+ " </nodes>" +
+ " </content>" +
+ "</services>";
+ }
+
+ private static final String resourcesReductionOverride =
+ "<validation-overrides>\n" +
+ " <allow until='2000-01-03'>resources-reduction</allow>\n" +
+ "</validation-overrides>\n";
+} \ No newline at end of file