diff options
author | Jon Bratseth <bratseth@gmail.com> | 2020-04-08 13:27:51 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@gmail.com> | 2020-04-08 13:27:51 +0200 |
commit | df8cf4a64723a469916f4307c52de6ad62b7f577 (patch) | |
tree | bc484b7a73287ceeb61b9d331848c4067d11cb3e /config-model | |
parent | 2f736a2961ccbe3977877c143e390668ae7de8bf (diff) |
Validate resource changes by the lower limit
Diffstat (limited to 'config-model')
10 files changed, 109 insertions, 59 deletions
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java index 696ce4195eb..3fb7ba6bc3a 100644 --- a/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java +++ b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java @@ -16,6 +16,7 @@ import com.yahoo.config.model.api.EndpointCertificateSecrets; import com.yahoo.config.model.api.HostProvisioner; import com.yahoo.config.model.api.Model; import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.Provisioned; import com.yahoo.config.model.api.ValidationParameters; import com.yahoo.config.model.application.provider.BaseDeployLogger; import com.yahoo.config.model.application.provider.MockFileRegistry; @@ -79,6 +80,7 @@ public class DeployState implements ConfigDefinitionStore { private final Optional<String> wantedDockerImageRepo; private final Instant now; private final HostProvisioner provisioner; + private final Provisioned provisioned; public static DeployState createTestState() { return new Builder().build(); @@ -98,6 +100,7 @@ public class DeployState implements ConfigDefinitionStore { FileRegistry fileRegistry, DeployLogger deployLogger, Optional<HostProvisioner> hostProvisioner, + Provisioned provisioned, ModelContext.Properties properties, Version vespaVersion, Optional<ApplicationPackage> permanentApplicationPackage, @@ -121,6 +124,7 @@ public class DeployState implements ConfigDefinitionStore { this.previousModel = previousModel; this.accessLoggingEnabledByDefault = accessLoggingEnabledByDefault; this.provisioner = hostProvisioner.orElse(getDefaultModelHostProvisioner(applicationPackage)); + this.provisioned = provisioned; this.schemas = searchDocumentModel.getSchemas(); this.documentModel = searchDocumentModel.getDocumentModel(); this.permanentApplicationPackage = permanentApplicationPackage; @@ -152,6 +156,8 @@ public class DeployState implements ConfigDefinitionStore { } } + public Provisioned provisioned() { return provisioned; } + /** Get the global rank profile registry for this application. */ public final RankProfileRegistry rankProfileRegistry() { return rankProfileRegistry; } @@ -288,6 +294,7 @@ public class DeployState implements ConfigDefinitionStore { private FileRegistry fileRegistry = new MockFileRegistry(); private DeployLogger logger = new BaseDeployLogger(); private Optional<HostProvisioner> hostProvisioner = Optional.empty(); + private Provisioned provisioned = new Provisioned(); private Optional<ApplicationPackage> permanentApplicationPackage = Optional.empty(); private ModelContext.Properties properties = new TestProperties(); private Version version = new Version(1, 0, 0); @@ -321,6 +328,11 @@ public class DeployState implements ConfigDefinitionStore { return this; } + public Builder provisioned(Provisioned provisioned) { + this.provisioned = provisioned; + return this; + } + public Builder permanentApplicationPackage(Optional<ApplicationPackage> permanentApplicationPackage) { this.permanentApplicationPackage = permanentApplicationPackage; return this; @@ -400,6 +412,7 @@ public class DeployState implements ConfigDefinitionStore { fileRegistry, logger, hostProvisioner, + provisioned, properties, version, permanentApplicationPackage, diff --git a/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java b/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java index 298517b85f6..8706bb44ded 100644 --- a/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java +++ b/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java @@ -4,6 +4,7 @@ package com.yahoo.config.model.provision; import com.yahoo.collections.ListMap; import com.yahoo.collections.Pair; import com.yahoo.config.model.api.HostProvisioner; +import com.yahoo.config.model.api.Provisioned; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterResources; @@ -60,6 +61,8 @@ public class InMemoryProvisioner implements HostProvisioner { private final boolean useMaxResources; + private Provisioned provisioned = new Provisioned(); + /** Creates this with a number of nodes with resources 1, 3, 9, 1 */ public InMemoryProvisioner(int nodeCount) { this(nodeCount, defaultResources); @@ -85,9 +88,11 @@ public class InMemoryProvisioner implements HostProvisioner { this(Map.of(defaultResources, hosts.asCollection()), failOnOutOfCapacity, false, startIndexForClusters, retiredHostNames); } - public InMemoryProvisioner(Map<NodeResources, Collection<Host>> hosts, boolean failOnOutOfCapacity, + public InMemoryProvisioner(Map<NodeResources, Collection<Host>> hosts, + boolean failOnOutOfCapacity, boolean useMaxResources, - int startIndexForClusters, String ... retiredHostNames) { + int startIndexForClusters, + String ... retiredHostNames) { this.failOnOutOfCapacity = failOnOutOfCapacity; this.useMaxResources = useMaxResources; for (Map.Entry<NodeResources, Collection<Host>> hostsWithResources : hosts.entrySet()) @@ -125,6 +130,7 @@ public class InMemoryProvisioner implements HostProvisioner { @Override public List<HostSpec> prepare(ClusterSpec cluster, Capacity requested, ProvisionLogger logger) { + provisioned.add(cluster.id(), requested); if (useMaxResources) return prepare(cluster, requested.maxResources(), requested.isRequired(), requested.canFail()); else @@ -165,6 +171,12 @@ public class InMemoryProvisioner implements HostProvisioner { return allocation; } + /** Create a new provisioned instance to record provision requests to this and returns it */ + public Provisioned startProvisionedRecording() { + provisioned = new Provisioned(); + return provisioned; + } + private HostSpec retire(HostSpec host) { return new HostSpec(host.hostname(), host.aliases(), 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 c8f708e84f4..89cd5c2e735 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 @@ -23,11 +23,13 @@ import com.yahoo.config.model.api.FileDistribution; import com.yahoo.config.model.api.HostInfo; import ai.vespa.rankingexpression.importer.configmodelview.ImportedMlModels; import com.yahoo.config.model.api.Model; +import com.yahoo.config.model.api.Provisioned; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.producer.AbstractConfigProducer; import com.yahoo.config.model.producer.AbstractConfigProducerRoot; import com.yahoo.config.model.producer.UserConfigRepo; import com.yahoo.config.provision.AllocatedHosts; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.log.LogLevel; import com.yahoo.searchdefinition.RankProfile; import com.yahoo.searchdefinition.RankProfileRegistry; @@ -122,6 +124,8 @@ public final class VespaModel extends AbstractConfigProducerRoot implements Seri private final FileDistributor fileDistributor; + private final Provisioned provisioned; + /** Creates a Vespa Model from internal model types only */ public VespaModel(ApplicationPackage app) throws IOException, SAXException { this(app, new NullConfigModelRegistry()); @@ -162,6 +166,7 @@ public final class VespaModel extends AbstractConfigProducerRoot implements Seri configModelRegistry = new VespaConfigModelRegistry(configModelRegistry); VespaModelBuilder builder = new VespaDomBuilder(); this.applicationPackage = deployState.getApplicationPackage(); + this.provisioned = deployState.provisioned(); root = builder.getRoot(VespaModel.ROOT_CONFIGID, deployState, this); createGlobalRankProfiles(deployState.getDeployLogger(), deployState.getImportedModels(), @@ -611,11 +616,23 @@ public final class VespaModel extends AbstractConfigProducerRoot implements Seri return Collections.unmodifiableMap(id2producer); } - /** - * Returns this root's model repository - */ + /** Returns this root's model repository */ public ConfigModelRepo configModelRepo() { return 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<ClusterSpec.Id> allClusters() { + return hostSystem().getHosts().stream() + .map(HostResource::spec) + .filter(spec -> spec.membership().isPresent()) + .map(spec -> spec.membership().get().cluster().id()) + .collect(Collectors.toSet()); + } + + + } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java b/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java index f0e119606bf..631f4dab1a7 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java @@ -146,6 +146,7 @@ public class VespaModelFactory implements ModelFactory { .properties(modelContext.properties()) .vespaVersion(version()) .modelHostProvisioner(createHostProvisioner(modelContext)) + .provisioned(modelContext.provisioned()) .endpoints(modelContext.properties().endpoints()) .modelImporters(modelImporters) .zone(zone) diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidator.java index d14fe91a53b..162f6798462 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidator.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidator.java @@ -2,6 +2,8 @@ package com.yahoo.vespa.model.application.validation.change; import com.yahoo.config.model.api.ConfigChangeAction; +import com.yahoo.config.provision.Capacity; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.vespa.model.VespaModel; import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.application.api.ValidationOverrides; @@ -21,35 +23,32 @@ public class ClusterSizeReductionValidator implements ChangeValidator { @Override public List<ConfigChangeAction> validate(VespaModel current, VespaModel next, ValidationOverrides overrides, Instant now) { - for (ContainerCluster currentCluster : current.getContainerClusters().values()) { - ContainerCluster nextCluster = next.getContainerClusters().get(currentCluster.getName()); - if (nextCluster == null) continue; - validate(currentCluster.getContainers().size(), - nextCluster.getContainers().size(), - currentCluster.getName(), + for (var clusterId : current.allClusters()) { + Capacity currentCapacity = current.provisioned().all().get(clusterId); + Capacity nextCapacity = next.provisioned().all().get(clusterId); + if (currentCapacity == null || nextCapacity == null) continue; + validate(currentCapacity, + nextCapacity, + clusterId, overrides, now); } - - for (ContentCluster currentCluster : current.getContentClusters().values()) { - ContentCluster nextCluster = next.getContentClusters().get(currentCluster.getName()); - if (nextCluster == null) continue; - validate(currentCluster.getSearch().getSearchNodes().size(), - nextCluster.getSearch().getSearchNodes().size(), - currentCluster.getName(), - overrides, - now); - } - return Collections.emptyList(); } - private void validate(int currentSize, int nextSize, String clusterName, ValidationOverrides overrides, Instant now) { + private void validate(Capacity current, + Capacity next, + ClusterSpec.Id clusterId, + ValidationOverrides overrides, + Instant now) { + int currentSize = current.minResources().nodes(); + int nextSize = next.minResources().nodes(); // don't allow more than 50% reduction, but always allow to reduce size with 1 if ( nextSize < ((double)currentSize) * 0.5 && nextSize != currentSize - 1) overrides.invalid(ValidationId.clusterSizeReduction, - "Size reduction in '" + clusterName + "' is too large. Current size: " + currentSize + - ", new size: " + nextSize + ". New size must be at least 50% of the current size", + "Size reduction in '" + clusterId.value() + "' is too large: " + + "New min size must be at least 50% of the current min size. " + + "Current size: " + currentSize + ", new size: " + nextSize, now); } 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 8fdcf249bbc..5343a322382 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 @@ -5,6 +5,7 @@ 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.Capacity; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.NodeResources; import com.yahoo.vespa.model.HostResource; @@ -15,6 +16,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -27,35 +29,43 @@ 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); + for (var clusterId : current.allClusters()) { + Capacity currentCapacity = current.provisioned().all().get(clusterId); + Capacity nextCapacity = next.provisioned().all().get(clusterId); + if (currentCapacity == null || nextCapacity == null) continue; + validate(currentCapacity, nextCapacity, clusterId, overrides, now); } return List.of(); } - private void validate(NodeResources currentResources, NodeResources nextResources, ClusterSpec.Id clusterId, - ValidationOverrides overrides, Instant now) { + private void validate(Capacity current, + Capacity next, + ClusterSpec.Id clusterId, + ValidationOverrides overrides, + Instant now) { + if (current.minResources().nodeResources() == NodeResources.unspecified) return; + if (next.minResources().nodeResources() == NodeResources.unspecified) return; + 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())) + validateResource("vCPU", + current.minResources().nodeResources().vcpu(), + next.minResources().nodeResources().vcpu()), + validateResource("memory GB", + current.minResources().nodeResources().memoryGb(), + next.minResources().nodeResources().memoryGb()), + validateResource("disk GB", + current.minResources().nodeResources().diskGb(), + next.minResources().nodeResources().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); + "Resource reduction in '" + clusterId.value() + "' is too large. " + + String.join(" ", illegalChanges) + + " New min resources must be at least 50% of the current min resources", + now); } private static Optional<String> validateResource(String resourceName, double currentValue, double nextValue) { @@ -64,15 +74,4 @@ public class ResourcesReductionValidator implements ChangeValidator { return Optional.of(String.format(Locale.ENGLISH ,"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/config/model/MockModelContext.java b/config-model/src/test/java/com/yahoo/config/model/MockModelContext.java index 38e438e4d3a..f8ab3cc54c8 100644 --- a/config-model/src/test/java/com/yahoo/config/model/MockModelContext.java +++ b/config-model/src/test/java/com/yahoo/config/model/MockModelContext.java @@ -9,6 +9,7 @@ import com.yahoo.config.model.api.ConfigDefinitionRepo; import com.yahoo.config.model.api.HostProvisioner; import com.yahoo.config.model.api.Model; import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.Provisioned; import com.yahoo.config.model.application.provider.BaseDeployLogger; import com.yahoo.config.model.application.provider.MockFileRegistry; import com.yahoo.config.model.application.provider.StaticConfigDefinitionRepo; @@ -53,6 +54,9 @@ public class MockModelContext implements ModelContext { } @Override + public Provisioned provisioned() { return new Provisioned(); } + + @Override public DeployLogger deployLogger() { return new BaseDeployLogger(); } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java index 09e4b377085..915b3c01e1b 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java @@ -6,6 +6,7 @@ import com.yahoo.collections.Pair; import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.model.api.ConfigChangeAction; import com.yahoo.config.model.api.HostProvisioner; +import com.yahoo.config.model.api.Provisioned; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.deploy.TestProperties; import com.yahoo.config.model.provision.InMemoryProvisioner; @@ -32,7 +33,7 @@ import static com.yahoo.config.model.test.MockApplicationPackage.MUSIC_SEARCHDEF */ public class ValidationTester { - private final HostProvisioner hostProvisioner; + private final InMemoryProvisioner hostProvisioner; /** Creates a validation tester with 1 node available */ public ValidationTester() { @@ -45,7 +46,7 @@ public class ValidationTester { } /** Creates a validation tester with a given host provisioner */ - public ValidationTester(HostProvisioner hostProvisioner) { + public ValidationTester(InMemoryProvisioner hostProvisioner) { this.hostProvisioner = hostProvisioner; } @@ -63,6 +64,7 @@ public class ValidationTester { Environment environment, String validationOverrides) { Instant now = LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE).atStartOfDay().atZone(ZoneOffset.UTC).toInstant(); + Provisioned provisioned = hostProvisioner.startProvisionedRecording(); ApplicationPackage newApp = new MockApplicationPackage.Builder() .withServices(services) .withSchemas(ImmutableList.of(MUSIC_SEARCHDEFINITION, BOOK_SEARCHDEFINITION)) @@ -77,6 +79,7 @@ public class ValidationTester { .applicationPackage(newApp) .properties(new TestProperties().setHostedVespa(true)) .modelHostProvisioner(hostProvisioner) + .provisioned(provisioned) .now(now); if (previousModel != null) deployStateBuilder.previousModel(previousModel); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java index 895aa4f6a36..87684aca174 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java @@ -27,7 +27,9 @@ public class ClusterSizeReductionValidatorTest { fail("Expected exception due to cluster size reduction"); } catch (IllegalArgumentException expected) { - assertEquals("cluster-size-reduction: Size reduction in 'default' is too large. Current size: 30, new size: 14. New size must be at least 50% of the current size. " + + assertEquals("cluster-size-reduction: Size reduction in 'default' is too large: " + + "New min size must be at least 50% of the current min size. " + + "Current size: 30, new size: 14. " + ValidationOverrides.toAllowMessage(ValidationId.clusterSizeReduction), Exceptions.toMessageString(expected)); } 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 1322a9061ed..9a363789798 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 @@ -30,7 +30,7 @@ public class ResourcesReductionValidatorTest { 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. " + + "Current memory GB: 64.00, new: 16.00. New min resources must be at least 50% of the current min resources. " + ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction), Exceptions.toMessageString(expected)); } @@ -45,7 +45,7 @@ public class ResourcesReductionValidatorTest { } 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. " + + "New min resources must be at least 50% of the current min resources. " + ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction), Exceptions.toMessageString(expected)); } |