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 4ebca99f0c8e..37cac5fa5029 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 @@ -25,10 +25,12 @@ public enum ValidationId { accessControl("access-control"), // Internal use, used in zones where there should be no access-control globalEndpointChange("global-endpoint-change"), // Changing global endpoints zoneEndpointChange("zone-endpoint-change"), // Changing zone (possibly private) endpoint settings - redundancyIncrease("redundancy-increase"), // Increasing redundancy - may easily cause feed blocked redundancyOne("redundancy-one"), // redundancy=1 requires a validation override on first deployment pagedSettingRemoval("paged-setting-removal"), // May cause content nodes to run out of memory - certificateRemoval("certificate-removal"); // Remove data plane certificates + certificateRemoval("certificate-removal"), // Remove data plane certificates + + @Deprecated + redundancyIncrease("redundancy-increase"); // Not in use. TODO: Remove on Vespa 9 private final String id; diff --git a/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java b/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java index e8103f1d1df1..e6011f342133 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java @@ -644,12 +644,12 @@ public ConfigModelRepo configModelRepo() { /** If provisioning through the node repo, returns the provision requests issued during build of this */ public Provisioned provisioned() { return provisioned; } - /** Returns the id of all clusters in this */ - public Set allClusters() { + /** Returns the spedc of all clusters in this */ + public Set allClusters() { return hostSystem().getHosts().stream() .map(HostResource::spec) .filter(spec -> spec.membership().isPresent()) - .map(spec -> spec.membership().get().cluster().id()) + .map(spec -> spec.membership().get().cluster()) .collect(Collectors.toCollection(LinkedHashSet::new)); } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java index a4b08b5c7fd3..77be2278c5d4 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java @@ -49,10 +49,10 @@ private void validateBudget(BigDecimal budget, Context context, var application = context.model().applicationPackage().getApplicationId(); var maxSpend = 0.0; - for (var id : context.model().allClusters()) { - if (adminClusterIds(context.model()).contains(id)) continue; - var cluster = context.model().provisioned().clusters().get(id); - var capacity = context.model().provisioned().capacities().getOrDefault(id, zeroCapacity); + for (var spec : context.model().allClusters()) { + if (adminClusterIds(context.model()).contains(spec.id())) continue; + var cluster = context.model().provisioned().clusters().get(spec.id()); + var capacity = context.model().provisioned().capacities().getOrDefault(spec.id(), zeroCapacity); maxSpend += capacityPolicies.applyOn(capacity, cluster.isExclusive()).maxResources().cost(); } 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 9fc2f48dd764..28d913c8d724 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,7 +17,6 @@ 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.NodeResourceChangeValidator; -import com.yahoo.vespa.model.application.validation.change.RedundancyIncreaseValidator; import com.yahoo.vespa.model.application.validation.change.ResourcesReductionValidator; import com.yahoo.vespa.model.application.validation.change.RestartOnDeployForLocalLLMValidator; import com.yahoo.vespa.model.application.validation.change.RestartOnDeployForOnnxModelChangesValidator; @@ -128,7 +127,6 @@ private static void validateChanges(Execution execution) { new ResourcesReductionValidator().validate(execution); new ContainerRestartValidator().validate(execution); new NodeResourceChangeValidator().validate(execution); - new RedundancyIncreaseValidator().validate(execution); new CertificateRemovalChangeValidator().validate(execution); new RedundancyValidator().validate(execution); new RestartOnDeployForOnnxModelChangesValidator().validate(execution); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidator.java index ce24d11121ce..f41d4050e0b3 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidator.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidator.java @@ -26,12 +26,12 @@ public class NodeResourceChangeValidator implements ChangeValidator { @Override public void validate(ChangeContext context) { - for (ClusterSpec.Id clusterId : context.previousModel().allClusters()) { - Optional currentResources = resourcesOf(clusterId, context.previousModel()); - Optional nextResources = resourcesOf(clusterId, context.model()); + for (ClusterSpec cluster : context.previousModel().allClusters()) { + Optional currentResources = resourcesOf(cluster, context.previousModel()); + Optional nextResources = resourcesOf(cluster, context.model()); if (currentResources.isEmpty() || nextResources.isEmpty()) continue; // new or removed cluster if ( changeRequiresRestart(currentResources.get(), nextResources.get())) - createRestartActionsFor(clusterId, context.previousModel()).forEach(context::require); + createRestartActionsFor(cluster, context.previousModel()).forEach(context::require); } } @@ -39,19 +39,19 @@ private boolean changeRequiresRestart(NodeResources currentResources, NodeResour return currentResources.memoryGiB() != nextResources.memoryGiB(); } - private Optional resourcesOf(ClusterSpec.Id clusterId, VespaModel model) { + private Optional resourcesOf(ClusterSpec cluster, VespaModel model) { return model.allocatedHosts().getHosts().stream().filter(host -> host.membership().isPresent()) - .filter(host -> host.membership().get().cluster().id().equals(clusterId)) + .filter(host -> host.membership().get().cluster().id().equals(cluster.id())) .findFirst() .map(HostSpec::advertisedResources); } - private List createRestartActionsFor(ClusterSpec.Id clusterId, VespaModel model) { - ApplicationContainerCluster containerCluster = model.getContainerClusters().get(clusterId.value()); + private List createRestartActionsFor(ClusterSpec cluster, VespaModel model) { + ApplicationContainerCluster containerCluster = model.getContainerClusters().get(cluster.id().value()); if (containerCluster != null) return createRestartActionsFor(containerCluster); - ContentCluster contentCluster = model.getContentClusters().get(clusterId.value()); + ContentCluster contentCluster = model.getContentClusters().get(cluster.id().value()); if (contentCluster != null) return createRestartActionsFor(contentCluster); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/RedundancyIncreaseValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/RedundancyIncreaseValidator.java deleted file mode 100644 index 2b30e1a337d8..000000000000 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/RedundancyIncreaseValidator.java +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright Vespa.ai. 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.vespa.model.application.validation.Validation.ChangeContext; -import com.yahoo.vespa.model.content.cluster.ContentCluster; - -/** - * Checks that redundancy is not increased (without a validation override), - * as that may easily cause the cluster to run out of resources. - * - * @author bratseth - */ -public class RedundancyIncreaseValidator implements ChangeValidator { - - @Override - public void validate(ChangeContext context) { - for (ContentCluster currentCluster : context.previousModel().getContentClusters().values()) { - ContentCluster nextCluster = context.model().getContentClusters().get(currentCluster.getSubId()); - if (nextCluster == null) continue; - if (redundancyOf(nextCluster) > redundancyOf(currentCluster)) { - context.invalid(ValidationId.redundancyIncrease, - "Increasing redundancy from " + redundancyOf(currentCluster) + " to " + - redundancyOf(nextCluster) + " in '" + currentCluster + ". " + - "This is a safe operation but verify that you have room for a " + - redundancyOf(nextCluster) + "/" + redundancyOf(currentCluster) + "x increase " + - "in content size"); - } - } - } - - private int redundancyOf(ContentCluster cluster) { return cluster.getRedundancy().effectiveFinalRedundancy(); } - -} 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 index a1f8a97d47f3..391831c7c719 100644 --- 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 @@ -7,14 +7,17 @@ import com.yahoo.config.provision.NodeResources; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.application.validation.Validation.ChangeContext; +import com.yahoo.vespa.model.content.cluster.ContentCluster; /** - * Checks that no cluster sizes are reduced too much in one go. + * Checks that resources per document per node is reduced too much in one go. * * @author bratseth */ public class ResourcesReductionValidator implements ChangeValidator { + private static final double maxAllowedUtilizationIncrease = 2.0; // Allow max doubling of utilization + @Override public void validate(ChangeContext context) { for (var clusterId : context.previousModel().allClusters()) { @@ -23,35 +26,66 @@ public void validate(ChangeContext context) { } } - private void validate(ClusterSpec.Id clusterId, ChangeContext context) { - ClusterResources current = clusterResources(clusterId, context.previousModel()); - ClusterResources next = clusterResources(clusterId, context.model()); - if (current == null || next == null) return; // No request recording - test + private void validate(ClusterSpec cluster, ChangeContext context) { + ClusterResources current = clusterResources(cluster.id(), context.previousModel()); + ClusterResources next = clusterResources(cluster.id(), context.model()); + if (current == null || next == null) return; + + double loadIncreasePerNode; + StringBuilder changes = new StringBuilder(); + if ( ! cluster.type().isContent()) { + loadIncreasePerNode = (double)current.nodes() / next.nodes(); + if (current.nodes() != next.nodes()) + changes.append("from ").append(current.nodes()).append(" nodes to ") + .append(next.nodes()).append(" nodes").append(", "); + } + else { // take data size per node given from redundancy and groups into account + ContentCluster currentCluster = context.previousModel().getContentClusters().get(cluster.id().value()); + ContentCluster nextCluster = context.model().getContentClusters().get(cluster.id().value()); + loadIncreasePerNode = + (double) nextCluster.getRedundancy().finalRedundancy() / currentCluster.getRedundancy().finalRedundancy() + * + (double) currentCluster.groupSize() / nextCluster.groupSize(); + if (nextCluster.getRedundancy().finalRedundancy() != currentCluster.getRedundancy().finalRedundancy()) + changes.append("redundancy from ").append(currentCluster.getRedundancy().finalRedundancy()) + .append(" to ").append(nextCluster.getRedundancy().finalRedundancy()).append(", "); + if (nextCluster.groupSize() != currentCluster.groupSize()) + changes.append("group size from ").append(currentCluster.groupSize()) + .append(" to ").append(nextCluster.groupSize()).append(" nodes, "); + } + if (current.nodeResources().isUnspecified() || next.nodeResources().isUnspecified()) { - // Self-hosted - unspecified resources; compare node count - int currentNodes = current.nodes(); - int nextNodes = next.nodes(); - if (nextNodes < 0.5 * currentNodes && nextNodes != currentNodes - 1) { - context.invalid(ValidationId.resourcesReduction, - "Size reduction in '" + clusterId.value() + "' is too large: " + - "To guard against mistakes, the new max nodes must be at least 50% of the current nodes. " + - "Current nodes: " + currentNodes + ", new nodes: " + nextNodes); - } + // Self-hosted: We don't know node resources so assume they are constant + if (loadIncreasePerNode > maxAllowedUtilizationIncrease) + invalid(changes, cluster, context); } else { - NodeResources currentResources = current.totalResources(); - NodeResources nextResources = next.totalResources(); - if (nextResources.vcpu() < 0.5 * currentResources.vcpu() || - nextResources.memoryGiB() < 0.5 * currentResources.memoryGiB() || - nextResources.diskGb() < 0.5 * currentResources.diskGb()) - context.invalid(ValidationId.resourcesReduction, - "Resource reduction in '" + clusterId.value() + "' is too large: " + - "To guard against mistakes, the new max resources must be at least 50% of the current " + - "max resources in all dimensions. " + - "Current: " + currentResources.withBandwidthGbps(0) + // (don't output bandwidth here) - ", new: " + nextResources.withBandwidthGbps(0)); + NodeResources currentResources = current.nodeResources(); + NodeResources nextResources = next.nodeResources(); + if (invalid(currentResources.vcpu(), nextResources.vcpu(), "vcpu", loadIncreasePerNode, changes) + || invalid(currentResources.memoryGiB(), nextResources.memoryGiB(), "memory GiB", loadIncreasePerNode, changes) + || invalid(currentResources.diskGb(), nextResources.diskGb(), "disk Gb", loadIncreasePerNode, changes)) + invalid(changes, cluster, context); } + } + + private boolean invalid(double current, double next, String resource, double loadIncreasePerNode, + StringBuilder changes) { + if (loadIncreasePerNode * current / next > maxAllowedUtilizationIncrease) { + if (current != next) + changes.append("from ").append(current).append(" to ").append(next) + .append(" ").append(resource).append(" per node, "); + return true; + } + return false; + } + private void invalid(StringBuilder changes, ClusterSpec cluster, ChangeContext context) { + changes.setLength(changes.length() - 2); // Remove last ", " + context.invalid(ValidationId.resourcesReduction, + "Effective resource reduction in " + cluster.id() + " is too large: " + + changes + + ". To protect against mistakes, changes causing load increases of more than 100% are blocked"); } /** diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java index 5754699cf3ac..e083969f7b4f 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java @@ -475,6 +475,10 @@ public boolean isGloballyDistributed(NewDocumentType docType) { public Redundancy getRedundancy() { return redundancy; } + public int groupSize() { + return getNodeCount() / getRootGroup().getNumberOfLeafGroups(); + } + public ContentCluster setRedundancy(Redundancy redundancy) { this.redundancy = redundancy; return this; diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/RedundancyIncreaseValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/RedundancyIncreaseValidatorTest.java deleted file mode 100644 index ef94f04748f2..000000000000 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/RedundancyIncreaseValidatorTest.java +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright Vespa.ai. 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.provision.Environment; -import com.yahoo.vespa.model.VespaModel; -import com.yahoo.vespa.model.application.validation.ValidationTester; -import com.yahoo.yolean.Exceptions; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - -/** - * @author bratseth - */ -public class RedundancyIncreaseValidatorTest { - - private final ValidationTester tester = new ValidationTester(7); - - @Test - void testRedundancyIncreaseValidation() { - VespaModel previous = tester.deploy(null, getServices(2, 3, 1), Environment.prod, null, "contentClusterId.indexing").getFirst(); - try { - tester.deploy(previous, getServices(3, 3, 1), Environment.prod, null, "contentClusterId.indexing"); - fail("Expected exception due to redundancy increase"); - } - catch (IllegalArgumentException expected) { - assertEquals("redundancy-increase: " + - "Increasing redundancy from 2 to 3 in 'content cluster 'contentClusterId'. " + - "This is a safe operation but verify that you have room for a 3/2x increase in content size. " + - ValidationOverrides.toAllowMessage(ValidationId.redundancyIncrease), - Exceptions.toMessageString(expected)); - } - } - - @Test - void testRedundancyIncreaseValidationWithGroups() { - // Changing redundancy from 1 to 2 is allowed when having 2 nodes in 2 groups versus 3 nodes in 1 group - // (effective redundancy for the cluster is 2 in both cases) - VespaModel previous = tester.deploy(null, getServices(1, 2, 2), Environment.prod, null, "contentClusterId.indexing").getFirst(); - tester.deploy(previous, getServices(2, 3, 1), Environment.prod, null, "contentClusterId.indexing"); - } - - @Test - void testOverridingContentRemovalValidation() { - VespaModel previous = tester.deploy(null, getServices(2, 3, 1), Environment.prod, null, "contentClusterId.indexing").getFirst(); - tester.deploy(previous, getServices(3, 3, 1), Environment.prod, redundancyIncreaseOverride, "contentClusterId.indexing"); // Allowed due to override - } - - private static String getServices(int redundancy, int nodes, int groups) { - return "" + - " " + - " " + redundancy + "" + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - ""; - } - - private static final String redundancyIncreaseOverride = - "\n" + - " redundancy-increase\n" + - "\n"; - -} 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 index 36839e72e101..b3759263f1a4 100644 --- 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 @@ -45,9 +45,7 @@ void fail_when_reduction_by_over_50_percent() { tester.deploy(previous, contentServices(6, toResources), Environment.prod, null, CONTAINER_CLUSTER); fail("Expected exception due to resources reduction"); } catch (IllegalArgumentException expected) { - assertResourceReductionException(expected, - fromResources.multipliedBy(6), - toResources.multipliedBy(6)); + assertResourceReductionException(expected, "from 64.0 to 16.0 memory GiB per node"); } } @@ -60,9 +58,7 @@ void fail_when_reducing_multiple_resources_by_over_50_percent() { tester.deploy(previous, contentServices(6, toResources), Environment.prod, null, CONTAINER_CLUSTER); fail("Expected exception due to resources reduction"); } catch (IllegalArgumentException expected) { - assertResourceReductionException(expected, - fromResources.multipliedBy(6), - toResources.multipliedBy(6)); + assertResourceReductionException(expected, "from 8.0 to 3.0 vcpu per node"); } } @@ -93,9 +89,7 @@ void reduction_is_detected_when_going_from_unspecified_resources_container() { fail("Expected exception due to resources reduction"); } catch (IllegalArgumentException expected) { - assertResourceReductionException(expected, - defaultResources.multipliedBy(6), - toResources.multipliedBy(6)); + assertResourceReductionException(expected, "from 50.0 to 10.0 disk Gb per node"); } } @@ -108,9 +102,7 @@ void reduction_is_detected_when_going_to_unspecified_resources_container() { fail("Expected exception due to resources reduction"); } catch (IllegalArgumentException expected) { - assertResourceReductionException(expected, - fromResources.multipliedBy(6), - defaultResources.multipliedBy(6)); + assertResourceReductionException(expected, "from 3.0 to 1.0 vcpu per node"); } } @@ -122,9 +114,7 @@ void reduction_is_detected_when_going_from_unspecified_resources_content() { tester.deploy(previous, contentServices(6, toResources), Environment.prod, null, CONTAINER_CLUSTER); fail("Expected exception due to resources reduction"); } catch (IllegalArgumentException expected) { - assertResourceReductionException(expected, - defaultResources.multipliedBy(6), - toResources.multipliedBy(6)); + assertResourceReductionException(expected, "from 50.0 to 10.0 disk Gb per node"); } } @@ -137,9 +127,7 @@ void reduction_is_detected_when_going_to_unspecified_resources_content() { fail("Expected exception due to resources reduction"); } catch (IllegalArgumentException expected) { - assertResourceReductionException(expected, - fromResources.multipliedBy(6), - defaultResources.multipliedBy(6)); + assertResourceReductionException(expected, "from 3.0 to 1.0 vcpu per node"); } } @@ -154,9 +142,7 @@ void testSizeReductionValidationWithUnspecifiedResourcesHosted() { fail("Expected exception due to resources reduction"); } catch (IllegalArgumentException expected) { - assertResourceReductionException(expected, - defaultResources.multipliedBy(fromNodes), - defaultResources.multipliedBy(toNodes)); + assertResourceReductionException(expected, "group size from 30 to 14 nodes"); } } @@ -171,9 +157,9 @@ void testSizeReductionValidationSelfhosted() { fail("Expected exception due to resources reduction"); } catch (IllegalArgumentException expected) { - assertEquals("resources-reduction: Size reduction in 'default' is too large: " + - "To guard against mistakes, the new max nodes must be at least 50% of the current nodes. " + - "Current nodes: 10, new nodes: 4. " + + assertEquals("resources-reduction: Effective resource reduction in cluster 'default' is too large: " + + "group size from 10 to 4 nodes. " + + "To protect against mistakes, changes causing load increases of more than 100% are blocked. " + ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction), Exceptions.toMessageString(expected)); } @@ -195,12 +181,83 @@ void testOverridingSizeReductionValidation() { tester.deploy(previous, contentServices(14, null), Environment.prod, resourcesReductionOverride, CONTAINER_CLUSTER); // Allowed due to override } - private void assertResourceReductionException(Exception e, NodeResources currentResources, NodeResources newResources) { - assertEquals("resources-reduction: Resource reduction in 'default' is too large: " + - "To guard against mistakes, the new max resources must be at least 50% of the current max " + - "resources in all dimensions. " + - "Current: " + currentResources.withBandwidthGbps(0) + - ", new: " + newResources.withBandwidthGbps(0) + ". " + + @Test + void testRedundancyIncreaseValidation() { + VespaModel previous = tester.deploy(null, getServices(1, 3, 1), Environment.prod, null, "contentClusterId.indexing").getFirst(); + try { + tester.deploy(previous, getServices(3, 3, 1), Environment.prod, null, "contentClusterId.indexing"); + fail("Expected exception due to redundancy increase"); + } + catch (IllegalArgumentException expected) { + assertEquals("resources-reduction: " + + "Effective resource reduction in cluster 'contentClusterId' is too large: " + + "redundancy from 1 to 3. " + + "To protect against mistakes, changes causing load increases of more than 100% are blocked. " + + ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction), + Exceptions.toMessageString(expected)); + } + } + + @Test + void testNoRedundancyIncreaseValidationErrorWhenIncreasingGroupsAndNodes() { + VespaModel previous = tester.deploy(null, getServices(1, 2, 2), Environment.prod, null, "contentClusterId.indexing").getFirst(); + tester.deploy(previous, getServices(1, 4, 4), Environment.prod, null, "contentClusterId.indexing"); + } + + @Test + void testRedundancyIncreaseValidationWithGroups() { + // Changing redundancy from 1 to 2 is allowed when having 2 nodes in 2 groups versus 3 nodes in 1 group + // (effective redundancy for the cluster is 2 in both cases) + VespaModel previous = tester.deploy(null, getServices(1, 2, 2), Environment.prod, null, "contentClusterId.indexing").getFirst(); + tester.deploy(previous, getServices(2, 3, 1), Environment.prod, null, "contentClusterId.indexing"); + } + + @Test + void testRedundancyDecreaseWithTooLargeNodeDecrease() { + try { + VespaModel previous = tester.deploy(null, getServices(3, 7, 1), Environment.prod, null, "contentClusterId.indexing").getFirst(); + tester.deploy(previous, getServices(2, 2, 1), Environment.prod, null, "contentClusterId.indexing"); + fail("Expected exception"); + } + catch (IllegalArgumentException expected) { + assertEquals("resources-reduction: Effective resource reduction in cluster 'contentClusterId' is too large: " + + "redundancy from 3 to 2, group size from 7 to 2 nodes. " + + "To protect against mistakes, changes causing load increases of more than 100% are blocked. " + + ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction), + expected.getMessage()); + } + } + + @Test + void testOverridingContentRemovalValidation() { + VespaModel previous = tester.deploy(null, getServices(2, 3, 1), Environment.prod, null, "contentClusterId.indexing").getFirst(); + tester.deploy(previous, getServices(3, 3, 1), Environment.prod, redundancyIncreaseOverride, "contentClusterId.indexing"); // Allowed due to override + } + + private static String getServices(int redundancy, int nodes, int groups) { + return "" + + " " + + " " + redundancy + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + ""; + } + + private static final String redundancyIncreaseOverride = + "\n" + + " redundancy-increase\n" + + "\n"; + + private void assertResourceReductionException(Exception e, String change) { + assertEquals("resources-reduction: Effective resource reduction in cluster 'default' is too large: " + + change + ". " + + "To protect against mistakes, changes causing load increases of more than 100% are blocked. " + ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction), Exceptions.toMessageString(e)); } @@ -244,4 +301,5 @@ private static String contentServices(int nodes, NodeResources resources) { "\n" + " resources-reduction\n" + "\n"; + }