diff options
author | Øyvind Grønnesby <oyving@verizonmedia.com> | 2020-10-27 09:33:32 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-27 09:33:32 +0100 |
commit | 8d141c6e23668b7260260b17e09d68528cb53486 (patch) | |
tree | 70d4da8359d07045f2c1ce1ddbfdbf557155137b | |
parent | 67aff4ae8112210c354a028b5dca5ddaa0f70d6e (diff) | |
parent | c36b1bd081047f0e95c27cd38afac69ae880b158 (diff) |
Merge pull request #15007 from vespa-engine/andreer/basic-quota-subtraction
andreer/basic quota subtraction
10 files changed, 171 insertions, 19 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java index 2283bdce885..78f8197062c 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java @@ -31,7 +31,7 @@ public class DeploymentData { private final Optional<DockerImage> dockerImageRepo; private final Optional<AthenzDomain> athenzDomain; private final Optional<ApplicationRoles> applicationRoles; - private final Optional<Quota> quota; + private final Quota quota; public DeploymentData(ApplicationId instance, ZoneId zone, byte[] applicationPackage, Version platform, Set<ContainerEndpoint> containerEndpoints, @@ -39,7 +39,7 @@ public class DeploymentData { Optional<DockerImage> dockerImageRepo, Optional<AthenzDomain> athenzDomain, Optional<ApplicationRoles> applicationRoles, - Optional<Quota> quota) { + Quota quota) { this.instance = requireNonNull(instance); this.zone = requireNonNull(zone); this.applicationPackage = requireNonNull(applicationPackage); @@ -88,7 +88,7 @@ public class DeploymentData { return applicationRoles; } - public Optional<Quota> quota() { + public Quota quota() { return quota; } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java index 646279ceaa0..2f05c99ab66 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java @@ -1,7 +1,6 @@ // Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.api.integration.billing; -import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.hosted.controller.api.integration.user.User; @@ -17,7 +16,7 @@ public interface BillingController { PlanId getPlan(TenantName tenant); - Optional<Quota> getQuota(TenantName tenant, Environment environment); + Quota getQuota(TenantName tenant); /** * @return String containing error message if something went wrong. Empty otherwise diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java index 5a5e8821429..4b09c744537 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java @@ -1,7 +1,6 @@ // Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.api.integration.billing; -import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.hosted.controller.api.integration.user.User; @@ -33,8 +32,8 @@ public class MockBillingController implements BillingController { } @Override - public Optional<Quota> getQuota(TenantName tenant, Environment environment) { - return Optional.of(Quota.unlimited().withMaxClusterSize(5)); + public Quota getQuota(TenantName tenant) { + return Quota.unlimited().withMaxClusterSize(5); } @Override diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java index b8dadae6b7c..feedf1be04f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java @@ -39,6 +39,10 @@ public class Quota { return UNLIMITED; } + public boolean isUnlimited() { + return budget.isEmpty() && maxClusterSize().isEmpty(); + } + public Quota withBudget(BigDecimal budget) { return new Quota(maxClusterSize, Optional.ofNullable(budget)); } @@ -61,6 +65,11 @@ public class Quota { return budget; } + public Quota subtractUsage(double rate) { + if (budget().isEmpty()) return this; // (unlimited - rate) is still unlimited + return this.withBudget(budget().get().subtract(BigDecimal.valueOf(rate))); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java index 0955a5388ce..84ca2fb1a8e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java @@ -5,12 +5,15 @@ import com.google.common.collect.ImmutableSortedMap; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; +import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.vespa.hosted.controller.application.ApplicationActivity; import com.yahoo.vespa.hosted.controller.application.Deployment; +import com.yahoo.vespa.hosted.controller.application.QuotaUsage; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.tenant.Tenant; @@ -179,6 +182,19 @@ public class Application { .min(Comparator.naturalOrder()); } + /** Returns the total quota usage for this application */ + public QuotaUsage quotaUsage() { + return instances().values().stream() + .map(Instance::quotaUsage).reduce(QuotaUsage::add).orElse(QuotaUsage.none); + } + + /** Returns the total quota usage for this application, excluding one specific deployment */ + public QuotaUsage quotaUsage(ApplicationId application, ZoneId zone) { + return instances().values().stream() + .map(instance -> instance.quotaUsageExcluding(application, zone)) + .reduce(QuotaUsage::add).orElse(QuotaUsage.none); + } + /** Returns the set of deploy keys for this application. */ public Set<PublicKey> deployKeys() { return deployKeys; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index a09dc0589ed..513a18f8745 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -53,6 +53,7 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.ApplicationPackageValidator; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; +import com.yahoo.vespa.hosted.controller.application.DeploymentQuotaCalculator; import com.yahoo.vespa.hosted.controller.application.QuotaUsage; import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; @@ -544,24 +545,18 @@ public class ApplicationController { .filter(tenant-> tenant instanceof AthenzTenant) .map(tenant -> ((AthenzTenant)tenant).domain()); - Optional<Quota> quota = billingController.getQuota(application.tenant(), zone.environment()); - - if (platform.isBefore(Version.fromString("7.299"))) { - // there is a bug in the configuration model that makes the QuotaValidator fail if the budget - // parameter is used. make sure we don't send budget to deployments with these old versions. - // TODO: Remove once < 7.299 is no longer deployed in public and publiccd - quota = quota.map(Quota::withoutBudget); - } - if (zone.environment().isManuallyDeployed()) controller.applications().applicationStore().putMeta(new DeploymentId(application, zone), clock.instant(), applicationPackage.metaDataZip()); + Quota deploymentQuota = DeploymentQuotaCalculator.calculate(billingController.getQuota(application.tenant()), + asList(application.tenant()), application, zone, applicationPackage.deploymentSpec()); + ConfigServer.PreparedApplication preparedApplication = configServer.deploy(new DeploymentData(application, zone, applicationPackage.zippedContent(), platform, endpoints, endpointCertificateMetadata, dockerImageRepo, domain, - applicationRoles, quota)); + applicationRoles, deploymentQuota)); return new ActivateResult(new RevisionId(applicationPackage.hash()), preparedApplication.prepareResponse(), applicationPackage.zippedContent().length); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java index e2ff017caac..dcfc1cbc606 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java @@ -166,6 +166,19 @@ public class Instance { return change; } + /** Returns the total quota usage for this instance **/ + public QuotaUsage quotaUsage() { + return deployments.values().stream() + .map(Deployment::quota).reduce(QuotaUsage::add).orElse(QuotaUsage.none); + } + + /** Returns the total quota usage for this instance, excluding one deployment */ + public QuotaUsage quotaUsageExcluding(ApplicationId application, ZoneId zone) { + return deployments.values().stream() + .filter(d -> !(application.equals(id) && d.zone().equals(zone))) + .map(Deployment::quota).reduce(QuotaUsage::add).orElse(QuotaUsage.none); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -185,5 +198,4 @@ public class Instance { public String toString() { return "application '" + id + "'"; } - } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java new file mode 100644 index 00000000000..82d83ea3585 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java @@ -0,0 +1,63 @@ +package com.yahoo.vespa.hosted.controller.application; + +import com.yahoo.config.application.api.DeploymentSpec; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.Application; +import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +/** Calculates the quota to allocate to a deployment. */ +public class DeploymentQuotaCalculator { + + public static Quota calculate(Quota tenantQuota, + List<Application> tenantApps, + ApplicationId deployingApp, ZoneId deployingZone, + DeploymentSpec deploymentSpec) { + + if (tenantQuota.budget().isEmpty()) return tenantQuota; // Shortcut if there is no budget limit to care about. + + if (deployingZone.environment().isProduction()) return probablyEnoughForAll(tenantQuota, tenantApps, deployingApp, deploymentSpec); + + return getMaximumAllowedQuota(tenantQuota, tenantApps, deployingApp, deployingZone); + } + + /** Just get the maximum quota we are allowed to use. */ + private static Quota getMaximumAllowedQuota(Quota tenantQuota, List<Application> applications, + ApplicationId application, ZoneId zone) { + var usageOutsideDeployment = applications.stream() + .map(app -> app.quotaUsage(application, zone)) + .reduce(QuotaUsage::add).orElse(QuotaUsage.none); + return tenantQuota.subtractUsage(usageOutsideDeployment.rate()); + } + + /** + * We want to avoid applying a resource change to an instance in production when it seems likely + * that there will not be enough quota to apply this change to _all_ production instances. + * <p> + * To achieve this, we must make the assumption that all production instances will use + * the same amount of resources, and so equally divide the quota among them. + */ + private static Quota probablyEnoughForAll(Quota tenantQuota, List<Application> tenantApps, + ApplicationId application, DeploymentSpec deploymentSpec) { + + TenantAndApplicationId deployingApp = TenantAndApplicationId.from(application); + + var usageOutsideApplication = tenantApps.stream() + .filter(app -> !app.id().equals(deployingApp)) + .map(Application::quotaUsage).reduce(QuotaUsage::add).orElse(QuotaUsage.none); + + long productionDeployments = Math.max(1, deploymentSpec.instances().stream() + .flatMap(instance -> instance.zones().stream()) + .filter(zone -> zone.environment().isProduction()) + .count()); + + return tenantQuota.withBudget( + tenantQuota.subtractUsage(usageOutsideApplication.rate()) + .budget().get().divide(BigDecimal.valueOf(productionDeployments), + 5, RoundingMode.HALF_UP)); // 1/1000th of a cent should be accurate enough + } +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java index 8851c6ae409..13384b63c84 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java @@ -19,6 +19,14 @@ public class QuotaUsage { return rate; } + public QuotaUsage add(QuotaUsage addend) { + return create(rate + addend.rate); + } + + public QuotaUsage sub(QuotaUsage subtrahend) { + return create(rate - subtrahend.rate); + } + public static QuotaUsage create(OptionalDouble rate) { if (rate.isEmpty()) { return QuotaUsage.none; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java new file mode 100644 index 00000000000..675cb2f3f76 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java @@ -0,0 +1,51 @@ +package com.yahoo.vespa.hosted.controller.application; + +import com.yahoo.config.application.api.DeploymentSpec; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; + +public class DeploymentQuotaCalculatorTest { + + @Test + public void quota_is_divided_among_prod_instances() { + Quota calculated = DeploymentQuotaCalculator.calculate(Quota.unlimited().withBudget(10), List.of(), ApplicationId.defaultId(), ZoneId.defaultId(), + DeploymentSpec.fromXml( + "<deployment version='1.0'>\n" + + " <instance id='instance1'> \n" + + " <test />\n" + + " <staging />\n" + + " <prod>\n" + + " <region active=\"true\">us-east-1</region>\n" + + " <region active=\"false\">us-west-1</region>\n" + + " </prod>\n" + + " </instance>\n" + + " <instance id='instance2'>\n" + + " <perf/>\n" + + " <dev/>\n" + + " <prod>\n" + + " <region active=\"true\">us-north-1</region>\n" + + " </prod>\n" + + " </instance>\n" + + "</deployment>")); + assertEquals(10d/3, calculated.budget().get().doubleValue(), 1e-5); + } + + @Test + public void unlimited_quota_remains_unlimited() { + Quota calculated = DeploymentQuotaCalculator.calculate(Quota.unlimited(), List.of(), ApplicationId.defaultId(), ZoneId.defaultId(), DeploymentSpec.empty); + assertTrue(calculated.isUnlimited()); + } + + @Test + public void zero_quota_remains_zero() { + Quota calculated = DeploymentQuotaCalculator.calculate(Quota.zero(), List.of(), ApplicationId.defaultId(), ZoneId.defaultId(), DeploymentSpec.empty); + assertEquals(calculated.budget().get().doubleValue(), 0, 1e-5); + } + +}
\ No newline at end of file |