summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorØyvind Grønnesby <oyving@verizonmedia.com>2020-08-26 09:02:21 +0200
committerGitHub <noreply@github.com>2020-08-26 09:02:21 +0200
commit4746dcd27ad567eb6f00adf773e9ac8fd55ffd35 (patch)
treec70124061c2fb64397b99b5fd1c42a18897bd793
parent1fd041abeebe0fc9749df5dcf9e2649374624786 (diff)
Quotas in the configuration server (#14088)
* Create a quota JSON encoded parameter * Propagate quota from PrepareParams to ModelContext.Properties * Persist quota and read it back * Check maxClusterSize quota in Validator step * Default to Quota.empty() in TestProperties * Javadoc and authors * Fix parameter type after it was changed on master Co-authored-by: Andreas Eriksen <andreer@verizonmedia.com>
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java5
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/model/api/Quota.java72
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java8
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java38
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java1
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java52
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java18
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java8
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java3
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java21
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java11
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java25
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java1
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java9
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java11
-rw-r--r--vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java7
16 files changed, 280 insertions, 10 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java
index 64a2906b1be..413a7dbc62e 100644
--- a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java
+++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java
@@ -121,6 +121,11 @@ public interface ModelContext {
// TODO(bjorncs): Temporary feature flag
default double feedCoreThreadPoolSizeFactor() { return 1.0; }
+
+ default Quota quota() {
+ return Quota.empty();
+ }
+
}
}
diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/Quota.java b/config-model-api/src/main/java/com/yahoo/config/model/api/Quota.java
new file mode 100644
index 00000000000..cb600bc0a5e
--- /dev/null
+++ b/config-model-api/src/main/java/com/yahoo/config/model/api/Quota.java
@@ -0,0 +1,72 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.api;
+
+import com.yahoo.slime.Inspector;
+import com.yahoo.slime.Slime;
+import com.yahoo.slime.SlimeUtils;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Quota for the application deployed. If the application exceeds this quota, deployment will fail.
+ *
+ * @author ogronnesby
+ */
+public class Quota {
+ private final Optional<Integer> maxClusterSize;
+ private final Optional<Integer> budget;
+
+ public Quota(Optional<Integer> maybeClusterSize, Optional<Integer> budget) {
+ this.maxClusterSize = maybeClusterSize;
+ this.budget = budget;
+ }
+
+ public static Quota fromSlime(Inspector inspector) {
+ var clusterSize = SlimeUtils.optionalLong(inspector.field("clusterSize"));
+ var budget = SlimeUtils.optionalLong(inspector.field("budget"));
+ return new Quota(clusterSize.map(Long::intValue), budget.map(Long::intValue));
+ }
+
+ public Slime toSlime() {
+ var slime = new Slime();
+ var root = slime.setObject();
+ maxClusterSize.ifPresent(clusterSize -> root.setLong("clusterSize", clusterSize));
+ budget.ifPresent(b -> root.setLong("budget", b));
+ return slime;
+ }
+
+ public static Quota empty() {
+ return new Quota(Optional.empty(), Optional.empty());
+ }
+
+ public Optional<Integer> maxClusterSize() {
+ return maxClusterSize;
+ }
+
+ public Optional<Integer> budget() {
+ return budget;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Quota quota = (Quota) o;
+ return Objects.equals(maxClusterSize, quota.maxClusterSize) &&
+ Objects.equals(budget, quota.budget);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(maxClusterSize, budget);
+ }
+
+ @Override
+ public String toString() {
+ return "Quota{" +
+ "maxClusterSize=" + maxClusterSize +
+ ", budget=" + budget +
+ '}';
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java
index abe14fbed32..fc799449379 100644
--- a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java
+++ b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java
@@ -7,6 +7,7 @@ import com.yahoo.config.model.api.ConfigServerSpec;
import com.yahoo.config.model.api.ContainerEndpoint;
import com.yahoo.config.model.api.EndpointCertificateSecrets;
import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.Quota;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.HostName;
@@ -46,6 +47,7 @@ public class TestProperties implements ModelContext.Properties {
private Optional<EndpointCertificateSecrets> endpointCertificateSecrets = Optional.empty();
private AthenzDomain athenzDomain;
private ApplicationRoles applicationRoles;
+ private Quota quota = Quota.empty();
@Override public boolean multitenant() { return multitenant; }
@Override public ApplicationId applicationId() { return applicationId; }
@@ -78,6 +80,7 @@ public class TestProperties implements ModelContext.Properties {
@Override public boolean skipCommunicationManagerThread() { return false; }
@Override public boolean skipMbusRequestThread() { return false; }
@Override public boolean skipMbusReplyThread() { return false; }
+ @Override public Quota quota() { return quota; }
public TestProperties setJvmGCOptions(String gcOptions) {
jvmGCOptions = gcOptions;
@@ -164,6 +167,11 @@ public class TestProperties implements ModelContext.Properties {
return this;
}
+ public TestProperties setQuota(Quota quota) {
+ this.quota = quota;
+ return this;
+ }
+
public static class Spec implements ConfigServerSpec {
private final String hostName;
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
new file mode 100644
index 00000000000..6670b7ce94a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java
@@ -0,0 +1,38 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.vespa.model.VespaModel;
+
+import java.util.stream.Collectors;
+
+/**
+ * Checks that the generated model does not have resources that exceeds the given quota.
+ *
+ * @author ogronnesby
+ */
+public class QuotaValidator extends Validator {
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+ var quota = deployState.getProperties().quota();
+ quota.maxClusterSize().ifPresent(maxClusterSize -> validateMaxClusterSize(maxClusterSize, model));
+ }
+
+ /** Check that all clusters in the application do not exceed the quota max cluster size. */
+ private void validateMaxClusterSize(int maxClusterSize, VespaModel model) {
+ var invalidClusters = model.allClusters().stream()
+ .filter(clusterId -> {
+ var cluster = model.provisioned().all().get(clusterId);
+ var clusterSize = cluster.maxResources().nodes();
+ return clusterSize > maxClusterSize;
+ })
+ .map(ClusterSpec.Id::value)
+ .collect(Collectors.toList());
+
+ if (!invalidClusters.isEmpty()) {
+ var clusterNames = String.join(", ", invalidClusters);
+ throw new IllegalArgumentException("Clusters " + clusterNames + " exceeded max cluster size of " + maxClusterSize);
+ }
+ }
+}
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 22dd0289390..f3ccc2d3447 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
@@ -61,6 +61,7 @@ public class Validation {
new AccessControlFilterValidator().validate(model, deployState);
new CloudWatchValidator().validate(model, deployState);
new AwsAccessControlValidator().validate(model, deployState);
+ new QuotaValidator().validate(model, deployState);
List<ConfigChangeAction> result = Collections.emptyList();
if (deployState.getProperties().isFirstTimeDeployment()) {
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java
new file mode 100644
index 00000000000..f8aa3fd5298
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java
@@ -0,0 +1,52 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.model.api.Quota;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.provision.Environment;
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author ogronnesby
+ */
+public class QuotaValidatorTest {
+
+ private final Quota quota = new Quota(Optional.of(5), Optional.empty());
+
+ @Test
+ public void test_deploy_under_quota() {
+ var tester = new ValidationTester(5, new TestProperties().setHostedVespa(true).setQuota(quota));
+ tester.deploy(null, getServices("testCluster", 5), Environment.prod, null);
+ }
+
+ @Test
+ public void test_deploy_above_quota() {
+ var tester = new ValidationTester(6, new TestProperties().setHostedVespa(true).setQuota(quota));
+ try {
+ tester.deploy(null, getServices("testCluster", 6), Environment.prod, null);
+ fail();
+ } catch (RuntimeException e) {
+ assertEquals("Clusters testCluster exceeded max cluster size of 5", e.getMessage());
+ }
+ }
+
+ private static String getServices(String contentClusterId, int nodeCount) {
+ return "<services version='1.0'>" +
+ " <content id='" + contentClusterId + "' version='1.0'>" +
+ " <redundancy>1</redundancy>" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='" + nodeCount + "'/>" +
+ " </content>" +
+ "</services>";
+ }
+}
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 362a083993e..6961ffa682b 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
@@ -31,6 +31,7 @@ import static com.yahoo.config.model.test.MockApplicationPackage.MUSIC_SEARCHDEF
*/
public class ValidationTester {
+ private final TestProperties properties;
private final InMemoryProvisioner hostProvisioner;
/** Creates a validation tester with 1 node available */
@@ -38,14 +39,25 @@ public class ValidationTester {
this(1);
}
+ /** Creates a validation tester with number of nodes available and the given test properties */
+ public ValidationTester(int nodeCount, TestProperties properties) {
+ this(new InMemoryProvisioner(nodeCount), properties);
+ }
+
+ /** Creates a validation tester with a given host provisioner */
+ public ValidationTester(InMemoryProvisioner hostProvisioner) {
+ this(hostProvisioner, new TestProperties().setHostedVespa(true));
+ }
+
/** Creates a validation tester with a number of nodes available */
public ValidationTester(int nodeCount) {
- this(new InMemoryProvisioner(nodeCount));
+ this(new InMemoryProvisioner(nodeCount), new TestProperties().setHostedVespa(true));
}
/** Creates a validation tester with a given host provisioner */
- public ValidationTester(InMemoryProvisioner hostProvisioner) {
+ public ValidationTester(InMemoryProvisioner hostProvisioner, TestProperties testProperties) {
this.hostProvisioner = hostProvisioner;
+ this.properties = testProperties;
}
/**
@@ -74,7 +86,7 @@ public class ValidationTester {
environment,
RegionName.defaultName()))
.applicationPackage(newApp)
- .properties(new TestProperties().setHostedVespa(true))
+ .properties(properties)
.modelHostProvisioner(hostProvisioner)
.provisioned(provisioned)
.now(now);
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
index 815cc0a3a18..3d4198c65a9 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
@@ -14,6 +14,7 @@ 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.Quota;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.DockerImage;
@@ -162,6 +163,7 @@ public class ModelContextImpl implements ModelContext {
private final Optional<AthenzDomain> athenzDomain;
private final Optional<ApplicationRoles> applicationRoles;
private final double feedCoreThreadPoolSizeFactor;
+ private final Quota quota;
public Properties(ApplicationId applicationId,
boolean multitenantFromConfig,
@@ -177,7 +179,8 @@ public class ModelContextImpl implements ModelContext {
FlagSource flagSource,
Optional<EndpointCertificateSecrets> endpointCertificateSecrets,
Optional<AthenzDomain> athenzDomain,
- Optional<ApplicationRoles> applicationRoles) {
+ Optional<ApplicationRoles> applicationRoles,
+ Optional<Quota> maybeQuota) {
this.applicationId = applicationId;
this.multitenant = multitenantFromConfig || hostedVespa || Boolean.getBoolean("multitenant");
this.configServerSpecs = configServerSpecs;
@@ -218,6 +221,7 @@ public class ModelContextImpl implements ModelContext {
this.applicationRoles = applicationRoles;
feedCoreThreadPoolSizeFactor = Flags.FEED_CORE_THREAD_POOL_SIZE_FACTOR.bindTo(flagSource)
.with(FetchVector.Dimension.APPLICATION_ID, applicationId.serializedForm()).value();
+ this.quota = maybeQuota.orElseGet(Quota::empty);
}
@Override
@@ -301,6 +305,8 @@ public class ModelContextImpl implements ModelContext {
@Override public boolean skipMbusRequestThread() { return skipMbusRequestThread; }
@Override public boolean skipMbusReplyThread() { return skipMbusReplyThread; }
@Override public double feedCoreThreadPoolSizeFactor() { return feedCoreThreadPoolSizeFactor; }
+
+ @Override public Quota quota() { return quota; }
}
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
index 6b42ca7fa95..7fc6b35722f 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
@@ -149,7 +149,8 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> {
.flatMap(new EndpointCertificateRetriever(secretStore)::readEndpointCertificateSecrets),
zkClient.readAthenzDomain(),
new ApplicationRolesStore(curator, TenantRepository.getTenantPath(tenant))
- .readApplicationRoles(applicationId));
+ .readApplicationRoles(applicationId),
+ zkClient.readQuota());
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
index 34974c00a84..1fea966503b 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
@@ -5,6 +5,7 @@ import com.yahoo.component.Version;
import com.yahoo.config.model.api.ApplicationRoles;
import com.yahoo.config.model.api.ContainerEndpoint;
import com.yahoo.config.model.api.EndpointCertificateMetadata;
+import com.yahoo.config.model.api.Quota;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.DockerImage;
@@ -42,6 +43,7 @@ public final class PrepareParams {
static final String ATHENZ_DOMAIN = "athenzDomain";
static final String APPLICATION_HOST_ROLE = "applicationHostRole";
static final String APPLICATION_CONTAINER_ROLE = "applicationContainerRole";
+ static final String QUOTA_PARAM_NAME = "quota";
private final ApplicationId applicationId;
private final TimeoutBudget timeoutBudget;
@@ -56,13 +58,14 @@ public final class PrepareParams {
private final Optional<DockerImage> dockerImageRepository;
private final Optional<AthenzDomain> athenzDomain;
private final Optional<ApplicationRoles> applicationRoles;
+ private final Optional<Quota> quota;
private PrepareParams(ApplicationId applicationId, TimeoutBudget timeoutBudget, boolean ignoreValidationErrors,
boolean dryRun, boolean verbose, boolean isBootstrap, Optional<Version> vespaVersion,
List<ContainerEndpoint> containerEndpoints, Optional<String> tlsSecretsKeyName,
Optional<EndpointCertificateMetadata> endpointCertificateMetadata,
Optional<DockerImage> dockerImageRepository, Optional<AthenzDomain> athenzDomain,
- Optional<ApplicationRoles> applicationRoles) {
+ Optional<ApplicationRoles> applicationRoles, Optional<Quota> quota) {
this.timeoutBudget = timeoutBudget;
this.applicationId = Objects.requireNonNull(applicationId);
this.ignoreValidationErrors = ignoreValidationErrors;
@@ -76,6 +79,7 @@ public final class PrepareParams {
this.dockerImageRepository = dockerImageRepository;
this.athenzDomain = athenzDomain;
this.applicationRoles = applicationRoles;
+ this.quota = quota;
}
public static class Builder {
@@ -93,6 +97,7 @@ public final class PrepareParams {
private Optional<DockerImage> dockerImageRepository = Optional.empty();
private Optional<AthenzDomain> athenzDomain = Optional.empty();
private Optional<ApplicationRoles> applicationRoles = Optional.empty();
+ private Optional<Quota> quota = Optional.empty();
public Builder() { }
@@ -187,11 +192,18 @@ public final class PrepareParams {
return this;
}
+ public Builder quota(String serialized) {
+ this.quota = (serialized == null)
+ ? Optional.empty()
+ : Optional.of(Quota.fromSlime(SlimeUtils.jsonToSlime(serialized).get()));
+ return this;
+ }
+
public PrepareParams build() {
return new PrepareParams(applicationId, timeoutBudget, ignoreValidationErrors, dryRun,
verbose, isBootstrap, vespaVersion, containerEndpoints, tlsSecretsKeyName,
endpointCertificateMetadata, dockerImageRepository, athenzDomain,
- applicationRoles);
+ applicationRoles, quota);
}
}
@@ -208,6 +220,7 @@ public final class PrepareParams {
.dockerImageRepository(request.getProperty(DOCKER_IMAGE_REPOSITORY))
.athenzDomain(request.getProperty(ATHENZ_DOMAIN))
.applicationRoles(ApplicationRoles.fromString(request.getProperty(APPLICATION_HOST_ROLE), request.getProperty(APPLICATION_CONTAINER_ROLE)))
+ .quota(request.getProperty(QUOTA_PARAM_NAME))
.build();
}
@@ -278,4 +291,8 @@ public final class PrepareParams {
public Optional<ApplicationRoles> applicationRoles() {
return applicationRoles;
}
+
+ public Optional<Quota> quota() {
+ return quota;
+ }
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
index 35bbc1a8233..7b40184e72a 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
@@ -17,6 +17,7 @@ import com.yahoo.config.model.api.EndpointCertificateMetadata;
import com.yahoo.config.model.api.EndpointCertificateSecrets;
import com.yahoo.config.model.api.FileDistribution;
import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.Quota;
import com.yahoo.config.provision.AllocatedHosts;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
@@ -216,7 +217,8 @@ public class SessionPreparer {
currentActiveApplicationSet.isEmpty(),
flagSource,
endpointCertificateSecrets,
- athenzDomain, applicationRoles);
+ athenzDomain, applicationRoles,
+ params.quota());
this.fileDistributionProvider = fileDistributionFactory.createProvider(serverDbSessionDir);
this.preparedModelsBuilder = new PreparedModelsBuilder(modelFactoryRegistry,
permanentApplicationPackage,
@@ -286,7 +288,8 @@ public class SessionPreparer {
logger,
prepareResult.getFileRegistries(),
prepareResult.allocatedHosts(),
- athenzDomain);
+ athenzDomain,
+ params.quota());
checkTimeout("write state to zookeeper");
}
@@ -335,7 +338,8 @@ public class SessionPreparer {
DeployLogger deployLogger,
Map<Version, FileRegistry> fileRegistryMap,
AllocatedHosts allocatedHosts,
- Optional<AthenzDomain> athenzDomain) {
+ Optional<AthenzDomain> athenzDomain,
+ Optional<Quota> quota) {
ZooKeeperDeployer zkDeployer = zooKeeperClient.createDeployer(deployLogger);
try {
zkDeployer.deploy(applicationPackage, fileRegistryMap, allocatedHosts);
@@ -345,6 +349,7 @@ public class SessionPreparer {
zooKeeperClient.writeVespaVersion(vespaVersion);
zooKeeperClient.writeDockerImageRepository(dockerImageRepository);
zooKeeperClient.writeAthenzDomain(athenzDomain);
+ zooKeeperClient.writeQuota(quota);
} catch (RuntimeException | IOException e) {
zkDeployer.cleanup();
throw new RuntimeException("Error preparing session", e);
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
index d28a322a05a..ff168e102b9 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
@@ -7,12 +7,15 @@ import com.yahoo.config.FileReference;
import com.yahoo.config.application.api.ApplicationPackage;
import com.yahoo.config.application.api.DeployLogger;
import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.model.api.Quota;
import com.yahoo.config.provision.AllocatedHosts;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.TenantName;
import com.yahoo.path.Path;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.SlimeUtils;
import com.yahoo.text.Utf8;
import com.yahoo.transaction.Transaction;
import com.yahoo.vespa.config.server.UserConfigDefinitionRepo;
@@ -25,10 +28,13 @@ import com.yahoo.vespa.curator.Curator;
import com.yahoo.vespa.curator.transaction.CuratorOperations;
import com.yahoo.vespa.curator.transaction.CuratorTransaction;
+import java.io.ByteArrayOutputStream;
import java.time.Instant;
import java.util.Optional;
import java.util.logging.Level;
+import static com.yahoo.yolean.Exceptions.uncheck;
+
/**
* Zookeeper client for a specific session. Path for a session is /config/v2/tenants/&lt;tenant&gt;/sessions/&lt;sessionid&gt;
* Can be used to read and write session status and create and get prepare and active barrier.
@@ -47,6 +53,7 @@ public class SessionZooKeeperClient {
private static final String CREATE_TIME_PATH = "createTime";
private static final String DOCKER_IMAGE_REPOSITORY_PATH = "dockerImageRepository";
private static final String ATHENZ_DOMAIN = "athenzDomain";
+ private static final String QUOTA_PATH = "quota";
private final Curator curator;
private final ConfigCurator configCurator;
private final TenantName tenantName;
@@ -179,6 +186,10 @@ public class SessionZooKeeperClient {
return sessionPath.append(ATHENZ_DOMAIN).getAbsolute();
}
+ private String quotaPath() {
+ return sessionPath.append(QUOTA_PATH).getAbsolute();
+ }
+
public void writeVespaVersion(Version version) {
configCurator.putData(versionPath(), version.toString());
}
@@ -240,6 +251,20 @@ public class SessionZooKeeperClient {
.map(AthenzDomain::from);
}
+ public void writeQuota(Optional<Quota> maybeQuota) {
+ maybeQuota.ifPresent(quota -> {
+ var bytes = uncheck(() -> SlimeUtils.toJsonBytes(quota.toSlime()));
+ configCurator.putData(quotaPath(), bytes);
+ });
+ }
+
+ public Optional<Quota> readQuota() {
+ if ( ! configCurator.exists(quotaPath())) return Optional.empty();
+ return Optional.ofNullable(configCurator.getData(quotaPath()))
+ .map(SlimeUtils::jsonToSlime)
+ .map(slime -> Quota.fromSlime(slime.get()));
+ }
+
/**
* Create necessary paths atomically for a new session.
*
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java
index 158b8ea55d2..9b827293516 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java
@@ -63,6 +63,7 @@ public class ModelContextImplTest {
flagSource,
null,
Optional.empty(),
+ Optional.empty(),
Optional.empty()),
Optional.empty(),
Optional.empty(),
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java
index bf21f800815..941f2726b0e 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java
@@ -84,6 +84,15 @@ public class PrepareParamsTest {
assertEquals("containerRole", applicationRoles.get().applicationContainerRole());
}
+ @Test
+ public void testQuotaParsing() {
+ var quotaParam = "{\"clusterSize\": 23, \"budget\": 23232323}";
+ var quotaEncoded = URLEncoder.encode(quotaParam, StandardCharsets.UTF_8);
+ var prepareParams = createParams(request + "&" + PrepareParams.QUOTA_PARAM_NAME + "=" + quotaEncoded, TenantName.from("foo"));
+ assertEquals(23, (int) prepareParams.quota().get().maxClusterSize().get());
+ assertEquals(23232323, (int) prepareParams.quota().get().budget().get());
+ }
+
// Create PrepareParams from a request (based on uri and tenant name)
private static PrepareParams createParams(String uri, TenantName tenantName) {
return PrepareParams.fromHttpRequest(
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java
index de55a6677ff..cc1137ad9d8 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.config.server.session;
import com.yahoo.config.FileReference;
+import com.yahoo.config.model.api.Quota;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.TenantName;
import com.yahoo.path.Path;
@@ -17,8 +18,10 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.time.Instant;
+import java.util.Optional;
import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@@ -137,6 +140,14 @@ public class SessionZooKeeperClientTest {
assertThat(zkc.readApplicationPackageReference(), is(testRef));
}
+ @Test
+ public void require_quota_written_and_parsed() {
+ var quota = Optional.of(new Quota(Optional.of(23), Optional.of(32)));
+ var zkc = createSessionZKClient(4);
+ zkc.writeQuota(quota);
+ assertEquals(quota, zkc.readQuota());
+ }
+
private void assertApplicationIdParse(long sessionId, String idString, String expectedIdString) {
SessionZooKeeperClient zkc = createSessionZKClient(sessionId);
String path = sessionPath(sessionId).append(SessionZooKeeperClient.APPLICATION_ID_PATH).getAbsolute();
diff --git a/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java b/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java
index 5084e6554cb..51a4fc167c7 100644
--- a/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java
+++ b/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java
@@ -124,6 +124,13 @@ public class SlimeUtils {
return Optional.of(inspector.asString()).filter(s -> !s.isEmpty());
}
+ public static Optional<Long> optionalLong(Inspector inspector) {
+ if (inspector.type() == Type.LONG) {
+ return Optional.of(inspector.asLong());
+ }
+ return Optional.empty();
+ }
+
public static Iterator<Inspector> entriesIterator(Inspector inspector) {
return new Iterator<>() {
private int current = 0;