summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorØyvind Grønnesby <oyving@verizonmedia.com>2020-10-27 09:33:32 +0100
committerGitHub <noreply@github.com>2020-10-27 09:33:32 +0100
commit8d141c6e23668b7260260b17e09d68528cb53486 (patch)
tree70d4da8359d07045f2c1ce1ddbfdbf557155137b
parent67aff4ae8112210c354a028b5dca5ddaa0f70d6e (diff)
parentc36b1bd081047f0e95c27cd38afac69ae880b158 (diff)
Merge pull request #15007 from vespa-engine/andreer/basic-quota-subtraction
andreer/basic quota subtraction
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java6
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java3
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java16
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java14
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java63
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java51
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